Compare commits

153 Commits
Author SHA1 Message Date
rodrigo 8f826ab223 pdf: novo refator do compress
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-28 19:56:28 -03:00
rodrigo cdc4134b16 pdf: refatorar compressPDF
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-28 19:15:54 -03:00
rodrigo d381d17435 pdf: accelerate OCR
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-27 20:25:46 -03:00
rodrigo ab73ea667e pdf: accelerate OCR
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-27 20:20:07 -03:00
rodrigo db7b832044 pdf: fix feature
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-27 20:15:07 -03:00
rodrigo f4afe83db0 pdf: fix route
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-27 19:02:18 -03:00
rodrigo 61a1631519 pdf: add new feature
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
2026-02-27 17:32:18 -03:00
rodrigo a63aaeb655 remove arquivo desnecessario
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-27 18:58:13 -03:00
rodrigo 48ea0c7303 undo commit 2025-07-27 18:57:22 -03:00
rodrigo 89b06db89e Limpeza de codigo
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-27 18:55:29 -03:00
rodrigo 928dfc0443 revert commit 2025-07-27 18:50:37 -03:00
rodrigo 24b5d04fbc frontend; backend: adiciona sistema novo de gravacao de provas
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-27 18:47:48 -03:00
rodrigo 0750b26208 frontend: fix dashboard PainelFinanceiro
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-27 17:26:25 -03:00
rodrigo 47f2d08b9b calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 17:01:49 -03:00
rodrigo 19f23a540a calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:52:50 -03:00
rodrigo 3e561b46bb calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:43:00 -03:00
rodrigo 3ae50e75d4 calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:24:08 -03:00
rodrigo d69d5b1b0e calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:20:59 -03:00
rodrigo 3f012552e7 calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:16:17 -03:00
rodrigo ae71161860 calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:14:08 -03:00
rodrigo 866a3fe539 calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:10:18 -03:00
rodrigo c37a0461bf dashboard: fix audiencias
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 12:24:20 -03:00
rodrigo 21932e5372 dashboard: fix audiencias
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 12:23:27 -03:00
rodrigo 626137276f dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:47:28 -03:00
rodrigo fc8e96cd58 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:46:08 -03:00
rodrigo 90a098e7db dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:43:12 -03:00
rodrigo 285d829c27 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:40:54 -03:00
rodrigo 0963cab395 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:08:05 -03:00
rodrigo 6300fb98a7 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 22:05:34 -03:00
rodrigo 0a67f95eaf dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:38:51 -03:00
rodrigo c03c6d1b5a dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:37:02 -03:00
rodrigo 061156e5e7 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:24:46 -03:00
rodrigo 71f5a56cbd dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:21:33 -03:00
rodrigo 683da3264d dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:19:21 -03:00
rodrigo 0475c34d64 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:17:35 -03:00
rodrigo 167766fdfd dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 21:09:34 -03:00
rodrigo a7c32aa128 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 20:45:55 -03:00
rodrigo b3d9ebe999 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 20:42:58 -03:00
rodrigo b713e24fbf dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 20:39:56 -03:00
rodrigo a4206a7611 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 20:28:16 -03:00
rodrigo 3d919350c8 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:47:49 -03:00
rodrigo 4d22c07151 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:46:14 -03:00
rodrigo 80df7310f3 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:38:23 -03:00
rodrigo 9160939de2 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:32:09 -03:00
rodrigo 9c2df341ea dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:28:40 -03:00
rodrigo 3c00956cf4 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:22:59 -03:00
rodrigo f5fb1e427d dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:21:28 -03:00
rodrigo 1e08f559ff dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:17:47 -03:00
rodrigo 1ab7bd8f36 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:14:01 -03:00
rodrigo f782fb36ad dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:12:10 -03:00
rodrigo af2f0ed649 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:08:55 -03:00
rodrigo 6b89a61dd7 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:07:35 -03:00
rodrigo 7e868dbc46 dashboard: reimaginacao
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-15 19:06:00 -03:00
Rodrigo B. de Sousa Martins b9e7de0ca0 backend: fix PDFService.ts 2025-06-11 11:57:33 -03:00
Rodrigo B. de Sousa Martins 0fef046d38 backend: fix PDFService.ts 2025-06-11 11:54:44 -03:00
Rodrigo B. de Sousa Martins 8f6b708d51 backend: fix PDFService.ts 2025-06-11 11:50:31 -03:00
Rodrigo B. de Sousa Martins db7446f19a frontend: possibilita edição manual do endereço do cliente
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo@martinsevieira.adv.br>
2025-05-26 13:32:38 -03:00
Rodrigo B. de Sousa Martins 59ea762227 frontend: fix PublicProvaUpload
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo@martinsevieira.adv.br>
2025-05-26 13:08:32 -03:00
rodrigo fe54b55af0 Fix - Size PDF
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-23 16:01:24 -03:00
rodrigo ecf692502f Fix localizacao prova
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-23 00:12:44 -03:00
rodrigo c72d4aad99 tenta fix pdfservice.js 2025-04-22 19:09:56 -03:00
rodrigo 8488bd7b84 tenta fix pdfservice.js 2025-04-22 16:37:51 -03:00
rodrigo 00a9fb9557 tenta fix pdfservice.js 2025-04-22 16:35:46 -03:00
rodrigo 0908378ca0 tenta fix pdfservice.js 2025-04-22 16:31:56 -03:00
rodrigo 46f538332e tenta fix pdfservice.js 2025-04-22 16:18:53 -03:00
rodrigo 494ad6ef9e tenta fix pdfservice.js 2025-04-22 16:08:41 -03:00
rodrigo 0a48bb414a tenta fix pdfservice.js 2025-04-22 16:04:34 -03:00
rodrigo 06f2d473e3 tenta fix pdfservice.js 2025-04-22 15:51:55 -03:00
rodrigo 5698266d3e tenta fix pdfservice.js 2025-04-22 15:29:53 -03:00
rodrigo 41d3963d67 tenta fix pdfservice.js 2025-04-22 15:24:39 -03:00
rodrigo 9b2e51732f tenta fix pdfservice.js 2025-04-22 15:22:23 -03:00
rodrigo 43cc832274 tenta fix pdfservice.js 2025-04-22 15:19:48 -03:00
rodrigo 4a189f82b2 tenta fix pdfservice.js 2025-04-22 15:16:40 -03:00
rodrigo f1f1ee4826 tenta fix pdfservice.js 2025-04-22 15:13:24 -03:00
rodrigo 705ef97ff8 tenta fix pdfservice.js 2025-04-22 15:09:42 -03:00
rodrigo 5b25414b9c tenta fix pdfservice.js 2025-04-22 15:06:49 -03:00
rodrigo e753ec8718 tenta fix pdfservice.js 2025-04-22 15:02:16 -03:00
rodrigo e208346614 tenta fix pdfservice.js 2025-04-22 15:00:47 -03:00
rodrigo 854531bce0 tenta fix pdfservice.js 2025-04-22 14:59:24 -03:00
rodrigo 597236c525 tenta fix pdfservice.js 2025-04-22 14:49:30 -03:00
rodrigo 6fdfc99e7d tenta fix pdfservice.js 2025-04-22 14:44:00 -03:00
rodrigo 045124be2f tenta fix pdfservice.js 2025-04-22 14:25:13 -03:00
rodrigo 8c3cd7a3bf tenta fix pdfservice.js 2025-04-22 12:17:20 -03:00
rodrigo 566ef2e123 tenta fix pdfservice.js 2025-04-22 12:14:27 -03:00
rodrigo 49dba7c9be tenta fix pdfservice.js 2025-04-22 12:06:42 -03:00
rodrigo 3529bffbbb tenta fix pdfservice.js 2025-04-22 12:04:19 -03:00
rodrigo db0c011b8b tenta fix pdfservice.js 2025-04-22 12:02:12 -03:00
rodrigo 19f0187227 tenta fix pdfservice.js 2025-04-22 11:57:14 -03:00
rodrigo 20b2f0e3c9 tenta fix pdfservice.js 2025-04-22 11:33:45 -03:00
rodrigo bbff8eb317 tenta fix pdfservice.js 2025-04-22 11:25:45 -03:00
rodrigo 06165db8bb tenta fix pdfservice.js 2025-04-22 11:19:59 -03:00
rodrigo 3966728ad4 tenta fix pdfservice.js 2025-04-22 11:14:37 -03:00
rodrigo 4708b9ec82 tenta fix pdfservice.js 2025-04-22 11:05:46 -03:00
rodrigo 1214171946 tenta fix pdfservice.js 2025-04-22 10:59:26 -03:00
rodrigo ce795cba58 tenta fix pdfservice.js 2025-04-22 10:54:57 -03:00
rodrigo 942222df6a tenta fix pdfservice.js 2025-04-22 10:53:11 -03:00
rodrigo 7f3313f3f3 tenta fix pdfservice.js 2025-04-22 10:50:44 -03:00
rodrigo a208caec55 tenta fix pdfservice.js 2025-04-22 10:47:12 -03:00
rodrigo e21dea582c tenta fix pdfservice.js 2025-04-22 10:44:51 -03:00
rodrigo c485960759 tenta fix pdfservice.js 2025-04-22 10:42:13 -03:00
rodrigo 79db5cc065 tenta fix pdfservice.js 2025-04-22 10:25:03 -03:00
rodrigo 92239d0a99 tenta fix pdfservice.js 2025-04-22 10:21:45 -03:00
rodrigo fefc6ce434 tenta fix pdfservice.js 2025-04-22 10:18:45 -03:00
rodrigo ac2891ce0d tenta fix pdfservice.js 2025-04-22 10:16:47 -03:00
rodrigo 07b677be29 tenta fix pdfservice.js 2025-04-22 10:14:11 -03:00
rodrigo bd70c10047 tenta fix pdfservice.js 2025-04-22 10:09:39 -03:00
rodrigo 85977d7229 tenta fix pdfservice.js 2025-04-22 10:04:59 -03:00
rodrigo 3406723bd4 tenta fix pdfservice.js 2025-04-22 10:01:02 -03:00
rodrigo cb47df6e88 tenta fix pdfservice.js 2025-04-22 09:56:28 -03:00
rodrigo 0121f67c65 tenta fix pdfservice.js 2025-04-22 09:51:56 -03:00
rodrigo a2bff3baeb tenta fix pdfservice.js 2025-04-22 09:46:30 -03:00
rodrigo defd9b33f5 tenta fix pdfservice.js 2025-04-21 23:11:33 -03:00
rodrigo 8abf8791be tenta fix pdfservice.js 2025-04-21 23:07:18 -03:00
rodrigo 5798152f7b tenta fix pdfservice.js 2025-04-21 23:05:41 -03:00
rodrigo 4726ce1dfa tenta fix pdfservice.js 2025-04-21 23:01:05 -03:00
rodrigo 00a9e698c6 tenta fix pdfservice.js 2025-04-21 22:57:55 -03:00
rodrigo bb5c9e25e2 tenta fix pdfservice.js 2025-04-21 22:50:08 -03:00
rodrigo a946f86cae tenta fix pdfservice.js 2025-04-21 22:46:09 -03:00
rodrigo 7656ff9807 tenta fix pdfservice.js 2025-04-21 22:42:44 -03:00
rodrigo ad3130a160 tenta fix pdfservice.js 2025-04-21 22:38:21 -03:00
rodrigo eeeae75919 tenta fix pdfservice.js 2025-04-21 22:08:29 -03:00
rodrigo 287ed34abf tenta fix pdfservice.js 2025-04-21 22:04:31 -03:00
rodrigo bd67cf49cc tenta fix pdfservice.js 2025-04-21 22:00:48 -03:00
rodrigo 3d044a962a tenta fix pdfservice.js 2025-04-21 21:58:18 -03:00
rodrigo 2a49dab8d5 tenta fix pdfservice.js 2025-04-21 21:51:48 -03:00
rodrigo 17078adc45 tenta fix pdfservice.js 2025-04-21 21:48:31 -03:00
rodrigo bf277416ff tenta fix pdfservice.js 2025-04-21 21:27:07 -03:00
rodrigo 5705978424 tenta fix pdfservice.js 2025-04-21 21:23:43 -03:00
rodrigo b0b7fba0f6 tenta fix pdfservice.js 2025-04-21 21:12:26 -03:00
rodrigo 0a651f94c0 tenta fix pdfservice.js 2025-04-21 21:09:46 -03:00
rodrigo 8cf2c23774 tenta fix pdfservice.js 2025-04-21 21:07:06 -03:00
rodrigo 2475bc6797 tenta fix pdfservice.js 2025-04-21 21:04:27 -03:00
rodrigo 570f3408c9 tenta fix pdfservice.js 2025-04-21 21:00:52 -03:00
rodrigo 8d12d45621 tenta fix pdfservice.js 2025-04-21 20:58:54 -03:00
rodrigo 3edd6ed317 tenta fix pdfservice.js 2025-04-21 20:55:41 -03:00
rodrigo fb17405fe9 tenta fix pdfservice.js 2025-04-21 20:47:49 -03:00
rodrigo f0fee9d22e tenta fix pdfservice.js 2025-04-21 20:37:34 -03:00
rodrigo f14f7ac3fd tenta fix pdfservice.js 2025-04-21 20:36:43 -03:00
rodrigo 76fa2c91ab tenta fix pdfservice.js 2025-04-21 20:35:21 -03:00
rodrigo 7e16bc8919 tenta fix pdfservice.js 2025-04-21 20:24:54 -03:00
rodrigo f3ff4f8939 tenta fix pdfservice.js 2025-04-21 20:22:23 -03:00
rodrigo e91294df22 tenta fix pdfservice.js 2025-04-21 20:17:45 -03:00
rodrigo 8a09720030 tenta fix pdfservice.js 2025-04-21 20:15:22 -03:00
rodrigo 3ccccc6792 tenta fix pdfservice.js 2025-04-21 20:06:37 -03:00
rodrigo f310f07987 tenta fix pdfservice.js 2025-04-21 19:52:13 -03:00
rodrigo e179fb33d7 tenta fix pdfservice.js 2025-04-21 19:48:27 -03:00
rodrigo 3a3c64f1b3 tenta fix pdfservice.js 2025-04-21 19:46:16 -03:00
rodrigo 73b571be71 tenta fix pdfservice.js 2025-04-21 19:42:27 -03:00
rodrigo 5786af03d5 tenta fix pdfservice.js 2025-04-21 19:39:03 -03:00
rodrigo 926a2934e4 tenta fix pdfservice.js 2025-04-21 19:36:52 -03:00
rodrigo a202bdc445 tenta fix pdfservice.js 2025-04-21 19:34:25 -03:00
rodrigo 6d487b5834 tenta fix pdfservice.js 2025-04-21 19:31:33 -03:00
rodrigo 4c4591fee5 fix atendimentos.ts
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-13 18:55:34 -03:00
242 changed files with 11463 additions and 10046 deletions

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# Configurações de Manipulação de PDF
# Tamanho máximo de arquivo em MB
PDF_MAX_FILE_SIZE=100
# Número máximo de arquivos por operação
PDF_MAX_FILES_COMPRESS=10
PDF_MAX_FILES_MERGE=50
PDF_MAX_FILES_IMAGES_TO_PDF=100
# Tempo de retenção de arquivos processados em horas
PDF_FILES_RETENTION_HOURS=24
# Qualidade padrão para compressão (50-100)
PDF_DEFAULT_QUALITY=95
# DPI padrão para conversão de imagens (150-600)
PDF_DEFAULT_DPI=300
# Idioma padrão para OCR (por, eng, spa)
PDF_DEFAULT_OCR_LANGUAGE=por
# Diretórios de armazenamento (relativos ao root do projeto)
PDF_TEMP_DIR=storage/temp/pdf-ops
PDF_OUTPUT_DIR=storage/pdf-output
PDF_UPLOAD_DIR=storage/temp/uploads
+19 -100
View File
@@ -1,110 +1,29 @@
module.exports = { module.exports = {
root: true, parser: '@typescript-eslint/parser',
env: {
browser: true,
es2021: true,
node: true,
},
extends: [ extends: [
"eslint:recommended", 'plugin:react/recommended',
"plugin:react/recommended", 'plugin:@typescript-eslint/recommended',
"plugin:react-hooks/recommended", 'plugin:react-hooks/recommended',
"plugin:@typescript-eslint/recommended", 'plugin:prettier/recommended'
"plugin:jsx-a11y/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"prettier",
], ],
parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true
}
}, },
ecmaVersion: 2021, plugins: ['react', '@typescript-eslint', 'react-hooks'],
sourceType: "module", rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'prettier/prettier': ['error', {}, { usePrettierrc: true }]
}, },
plugins: [
"react",
"react-hooks",
"@typescript-eslint",
"jsx-a11y",
"import",
"prettier",
],
settings: { settings: {
react: { react: {
version: "detect", version: 'detect'
}, }
"import/resolver": { }
typescript: {},
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
},
},
rules: {
// Regras essenciais para segurança e prevenção de erros
"no-console": ["warn", { allow: ["warn", "error", "info"] }],
"no-unused-vars": "off", // Desativado em favor da versão TypeScript
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-undef": "error",
"no-var": "error",
"prefer-const": "error",
eqeqeq: ["error", "always"],
// Regras específicas do React
"react/prop-types": "off", // Desnecessário com TypeScript
"react/react-in-jsx-scope": "off", // Não necessário no React 17+
"react/jsx-uses-react": "off", // Não necessário no React 17+
"react/jsx-key": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
// Regras de acessibilidade
"jsx-a11y/anchor-is-valid": "warn",
// Estilo e formatação
"prettier/prettier": "warn",
// Regras específicas de segurança
"no-eval": "error",
"no-implied-eval": "error",
// Regras para evitar SQL injection (útil para código de backend)
"@typescript-eslint/no-explicit-any": "warn",
// Regras para importações
"import/no-unresolved": "error",
"import/named": "error",
"import/default": "error",
"import/namespace": "error",
"import/order": [
"warn",
{
groups: [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
],
"newlines-between": "always",
alphabetize: { order: "asc", caseInsensitive: true },
},
],
},
overrides: [
// Regras específicas para arquivos de teste
{
files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
env: {
jest: true,
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
],
}; };
+1
View File
@@ -7,3 +7,4 @@ dist
.DS_Store .DS_Store
coverage coverage
.vscode .vscode
storage/
+12 -3
View File
@@ -1,11 +1,20 @@
FROM node:20-slim FROM node:20-slim
# Configurar Puppeteer para usar o Chromium do sistema
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
# Instalar dependências do sistema # Instalar dependências do sistema
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y chromium \
chromium \
libgconf-2-4 \ libgconf-2-4 \
postgresql-client \ postgresql-client \
&& rm -rf /var/lib/apt/lists/* graphicsmagick \
ghostscript \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf /usr/bin/chromium /usr/bin/chromium-browser
# Verificar a instalação do Chromium
RUN chromium --version
# Criar diretório da aplicação # Criar diretório da aplicação
WORKDIR /usr/src/app WORKDIR /usr/src/app
+100
View File
@@ -0,0 +1,100 @@
# Passos para ativar a funcionalidade de PDF
## 1. Instalar Dependências
Execute o seguinte comando para instalar todas as dependências necessárias:
```bash
npm install pdf-lib pdf-parse pdf2pic tesseract.js
npm install --save-dev @types/pdf-parse
```
## 2. Executar Migration do Banco de Dados
Execute a migration para criar a tabela de operações de PDF:
```bash
npx prisma migrate dev --name add_operacoes_pdf
```
Ou simplesmente:
```bash
npm run prisma:migrate
```
## 3. Gerar Cliente Prisma
Gere o cliente Prisma atualizado:
```bash
npm run prisma:generate
```
## 4. Criar Diretórios
Os diretórios serão criados automaticamente, mas você pode criá-los manualmente:
**No Windows (PowerShell):**
```powershell
New-Item -Path "storage\temp\pdf-ops" -ItemType Directory -Force
New-Item -Path "storage\pdf-output" -ItemType Directory -Force
New-Item -Path "storage\temp\uploads" -ItemType Directory -Force
```
**No Linux/Mac:**
```bash
mkdir -p storage/temp/pdf-ops
mkdir -p storage/pdf-output
mkdir -p storage/temp/uploads
```
## 5. Reiniciar o Servidor
Após instalar as dependências e executar as migrations, reinicie o servidor:
```bash
npm run dev:full
```
Ou apenas o backend:
```bash
npm run server
```
## Verificação
Após seguir todos os passos:
1. Acesse a aplicação no navegador
2. Faça login
3. Clique no item "PDFs" no menu lateral
4. Teste a funcionalidade de compressão enviando um PDF
## Troubleshooting
### Se continuar dando erro 404:
1. Verifique se o servidor foi reiniciado após as mudanças
2. Verifique se as dependências foram instaladas corretamente
3. Verifique os logs do servidor para erros de importação
### Se der erro de módulo não encontrado:
Execute:
```bash
npm install
npm run prisma:generate
```
### Se der erro no banco de dados:
Execute:
```bash
npx prisma migrate reset
npx prisma migrate dev
npm run prisma:generate
```
**ATENÇÃO:** O comando `migrate reset` apaga todos os dados do banco!
+26
View File
@@ -0,0 +1,26 @@
import express from "express";
import cors from "cors";
import { PrismaClient } from "@prisma/client";
import usuariosRoutes from "./routes/usuarios";
import clientesRoutes from "./routes/clientes";
import processosRoutes from "./routes/processos";
import audienciasRoutes from "./routes/audiencias";
import pdfRoutes from "./routes/pdf";
const app = express();
const prisma = new PrismaClient();
app.use(cors());
app.use(express.json());
app.use("/api/usuarios", usuariosRoutes);
app.use("/api/clientes", clientesRoutes);
app.use("/api/processos", processosRoutes);
app.use("/api/audiencias", audienciasRoutes);
app.use("/api/pdf", pdfRoutes);
app.get("/", (req, res) => {
res.json({ message: "API is running" });
});
export default app;
@@ -0,0 +1,20 @@
import { Request, Response } from "express";
import prisma from "../prisma/client";
// Lista todas as audiências
export async function getAudiencias(req: Request, res: Response) {
try {
const audiencias = await prisma.audiencia.findMany({
orderBy: { data: "asc" },
include: {
processo: {
select: { numero: true, nomeContraparte: true }
}
}
});
res.json(audiencias);
} catch (error) {
console.error("Erro ao buscar audiências:", error);
res.status(500).json({ error: "Erro ao buscar audiências" });
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Router } from "express";
import { getAudiencias } from "../controllers/audienciasController";
const router = Router();
// GET /api/audiencias - Lista todas as audiências
router.get("/", getAudiencias);
export default router;
+13
View File
@@ -0,0 +1,13 @@
version: '3.8'
services:
backend:
build: .
ports:
- "5000:5000"
environment:
- NODE_ENV=production
- CHROME_BIN=/usr/bin/google-chrome-stable
# ... outras variáveis de ambiente existentes ...
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
+596
View File
@@ -0,0 +1,596 @@
# Funcionalidade de Manipulação de PDFs
## Visão Geral
Sistema completo de manipulação de arquivos PDF com as seguintes funcionalidades:
### Recursos Implementados
1. **Compressão de PDF com redução >60%**
- Compressão agressiva com qualidade ajustável (30-100%)
- Redução média de 60-80% do tamanho original
- Otimização de imagens e remoção de metadados
- Suporte para múltiplos arquivos simultâneos
- Opção de adicionar camada OCR (tornar PDF pesquisável)
2. **Divisão de PDF**
- Por páginas específicas (ex: 1,3,5,7)
- Por intervalos (ex: 1-5, 6
- **Download em ZIP** quando múltiplos arquivos-10)
- Por chunks de tamanho fixo
3. **Mesclagem de PDFs**
- Combine até 50 PDFs em um único arquivo
- Preserva qualidade o
- Opção de adicionar camada OCR ao PDF mescladoriginal
- Ordem personalizável
4. **PDF para Imagens**
- Formatos: PNG, JPG, WEBP
- DPI ajustável (150-600)
- **Download em ZIP** de todas as imagens
- Qualidade configurável (50-100%)
- Conversão de páginas específicas ou todas
5. **Imagens para PDF**
- Opção de adicionar camada OCR (tornar PDF pesquisável)
- Suporta múltiplas imagens (até 100)
- Ajuste automático ao tamanho da página
- Suporta PNG, JPG, JPEG, WEBP
6. **OCR (Reconhecimento Ótico de Caracteres)**
- Extração de texto de PDFs escaneados
- Suporte para Português, Inglês e Espanhol
- Retorna confiança da extração
- Processamento por página
7. **Informações do PDF**
- Número de páginas
- Tamanho do arquivo
- Metadados (título, autor, criador, etc.)
- Datas de criação e modificação
## Instalação
### 1. Instalar Dependências
```bash
npm install pdf-lib pdf-parse pdf2pic tesseract.js archiver
npm install --save-dev @types/pdf-parse @types/archiver
```
Ou execute o script de instalação:
```bash
npm run install-pdf-deps
```
### 2. Instalar GraphicsMagick (Requerido para PDF → Imagens)
**Docker:**
O GraphicsMagick já está incluído no Dockerfile. Rebuild a imagem se necessário:
```bash
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
**Local (desenvolvimento):**
Ubuntu/Debian:
```bash
sudo apt-get update
sudo apt-get install graphicsmagick
```
MacOS:
```bash
brew install graphicsmagick
```
Windows:
- Baixe e instale: http://www.graphicsmagick.org/download.html
- Adicione ao PATH do sistema
Verifique a instalação:
```bash
gm version
```
### 3. Executar Migration do Banco de Dados
```bash
npm run prisma:migrate
```
Ou manualmente:
```bash
npx prisma migrate dev --name add_operacoes_pdf
```
### 4. Gerar Cliente Prisma
```bash
npm run prisma:generate
```
### 5. Criar Diretórios Necessários
Os diretórios serão criados automaticamente na primeira execução, mas você pode criá-los manualmente:
```bash
mkdir -p storage/temp/pdf-ops
mkdir -p storage/pdf-output
mkdir -p storage/temp/uploads
```
## Uso
### Frontend
Acesse a página de manipulação de PDFs através da rota:
```
/pdf
```
### API Endpoints
Todas as rotas requerem autenticação via Bearer token.
#### 1. Comprimir PDF
```http
POST /api/pdf/compress
Content-Type: multipart/form-data
Authorization: Bearer {token}
30-100, padrão 50 para máxima compressão)
- maxSizeMB: number (opcional)
- enableOCR: boolean (opcional, adiciona camada OCR ao PDF
- files: File[] (array de PDFs)
- quality: number (50-100, padrão 95)
- maxSizeMB: number (opcional)
```
#### 2. Dividir PDF
```http
POST /api/pdf/split
Content-Type: multipart/form-data
Authorization: Bearer {token}
Body:
- files: File (único PDF)
- splitType: 'pages' | 'range' | 'chunks'
- pages: string (ex: "1,2,3") - para splitType='pages'
- ranges: string (ex: "1-5,6-10") - para splitType='range'
- chunkSize: number - para splitType='chunks'
```
#### 3. Mesclar PDFs
```http
POST /api/pdf/merge
Content-Type: multipart/form-da
- enableOCR: boolean (opcional, adiciona camada OCR ao PDF mesclado)ta
Authorization: Bearer {token}
Body:
- files: File[] (mínimo 2 PDFs)
```
#### 4. PDF para Imagens
```http
POST /api/pdf/pdf-to-images
Content-Type: multipart/form-data
Authorization: Bearer {token}
Body:
- files: File (único PDF)
- format: 'png' | 'jpg' | 'webp' (padrão: png)
- quality: number (50-100, padrão: 95)
- density: number (DPI, 150-600, padrão: 300)
- pages: string (opcional, ex: "[1,2,3]" ou "all")
```
#### 5. Imagens para PDF
```http
POST /api/pdf/images-to-pdf
Content-Type: multipart/form-data
Authorization: Bearer {token}
- enableOCR: boolean (opcional, adiciona camada OCR ao PDF)
Body:
- files: File[] (array de imagens)
- fitToPage: boolean (padrão: true)
- pageSize: string (opcional, JSON: {"width":595,"height":842})
```
#### 6. OCR
```http
POST /api/pdf/ocr
Content-Type: multipart/form-data
Authorization: Bearer {token}
Body:
- files: File (único PDF)
- language: string ('por' | 'eng' | 'spa', padrão: 'por')
- pages: string (opcional, ex: "[1,2]" ou "all")
```
#### 7. Informações do PDF
```http
POST /api/pdf/info
Content-Type: multipart/form-data
Authorization: Bearer {token}
Body:
- files: File (único PDF)
```
#### 8. Histórico de Operações
```http
GET /api/pdf/history?page=1&limit=20
Authorization: Bearer {token}
```
#### 9. Download de Arquivo Processado
```http
GET /api/pdf/download/:operacaoId/:fileIndex
Authorization: Bearer {token}
```
## Estrutura de Resposta
### Sucesso (Compressão)
...],
"outputPaths": ["/path/compressed-1.pdf", "/path/compressed-2.pdf"],
"totalOriginalSize": 1048576,
"totalCompressedSize": 314572,
"reduction": 70.0,
"processingTime": 1234
}
```
### Sucesso (Split/PDF para Imagens - com ZIP)
```json
{
"success": true,
"operacaoId": "uuid",
"filesCreated": 10,
"outputPaths": ["/path/page-1.pdf", "/path/page-2.pdf", ...],
"zipPath": "/path/arquivos-uuid.zip",
"processingTime": 2345
],
"totalOriginalSize": 1048576,
"totalCompressedSize": 524288,
"totalReduction": 50.0,
"processingTime": 1234
}
```
### Sucesso (OCR)
```json
{
"success": true,
"operacaoId": "uuid",
"result": {
"text": "Texto extraído completo...",
"confidence": 95.5,
"pages": [
{
"pageNumber": 1,
"text": "Texto da página 1...",
"confidence": 96.2
}
]
},
"processingTime": 5678
}
```
### Erro
```json
{
"error": "Falha ao processar arquivo",
"message": "Detalhes específicos do erro"
}
```
## Modelo de Dados
### OperacaoPDF
```typescript
interface OperacaoPDF {
id: string;
tipoOperacao: 'COMPRIMIR' | 'DIVIDIR' | 'MESCLAR' | 'PDF_PARA_IMAGEM' | 'IMAGEM_PARA_PDF' | 'OCR';
status: 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO';
usuarioId: string;
arquivosOriginais: string[];
arquivosResultado: string[];
tamanhoOriginal: bigint;
tamanhoResultado: bigint | null;
parametros: any;
mensagemErro: string | null;
detalhesOCR: any | null;
tempoProcessamento: number | null;
criadoEm: Date;
atualizadoEm: Date;
}
```
## Segurança
- ✅ Autenticação obrigatória em todas as rotas
- ✅ Validação de tipos de arquivo (apenas PDF e imagens)
- ✅ Limite de tamanho de arquivo (100MB por arquivo)
- ✅ Limite de quantidade de arquivos por operação
- ✅ Sanitização de nomes de arquivo
- ✅ Isolamento de arquivos por operação
- ✅ Limpeza automática de arquivos temporários
## Performance
- ✅ Processamento assíncrono
- ✅ Otimização de memória com streams
- ✅ Compressão sem perda de qualidade perceptível
- ✅ Cache de workers de OCR
- ✅ Índices no banco de dados para consultas rápidas
- ✅ Limpeza automática de arquivos antigos
## Tratamento de Erros
Todos os erros são:
- Logados no console do servidor
- Registrados no banco de dados (tabela operacoes_pdf)
- Retornados ao cliente com mensagens descritivas
- Capturados por try-catch em todas as operações
## Limitações
- Tamanho máximo por arquivo: 100MB
- Máximo de arquivos para compressão: 10
- Máximo de arquivos para mesclagem: 50
- Máximo de imagens para conversão em PDF: 100
- Arquivos processados são mantidos por 24 horas
- **Timeout de operações:** 10 minutos (600 segundos)
- Operações longas (OCR, compressão de PDFs grandes) podem levar alguns minutos
- O sistema está configurado para suportar até 10 minutos de processamento
## Manutenção
### Limpeza de Arquivos Antigos
Execute periodicamente (recomendado: diariamente via cron):
```typescript
import { PDFService } from './src/services/pdfService';
// Remove arquivos com mais de 24 horas
await PDFService.cleanupOldFiles(24);
```
### Monitoramento
Consulte a tabela `operacoes_pdf` para:
- Estatísticas de uso
- Taxa de erro
- Tempo médio de processamento
- Operações mais utilizadas
```sql
-- Operações por tipo
SELECT "tipoOperacao", COUNT(*) as total
FROM operacoes_pdf
GROUP BY "tipoOperacao"
ORDER BY total DESC;
-- Taxa de sucesso
SELECT
status,
COUNT(*) as total,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentual
FROM operacoes_pdf
GROUP BY status;
-- Tempo médio por operação
SELECT
"tipoOperacao",
AVG("tempoProcessamento") as tempo_medio_ms,
MIN("tempoProcessamento") as tempo_min_ms,
MAX("tempoProcessamento") as tempo_max_ms
FROM operacoes_pdf
WHERE status = 'CONCLUIDO'
GROUP BY "tipoOperacao";
```
## Troubleshooting
### Erro: "Could not execute GraphicsMagick/ImageMagick"
**Sintoma:** Erro ao converter PDF para imagens:
```
Error: Could not execute GraphicsMagick/ImageMagick: gm "convert" ...
this most likely means the gm/convert binaries can't be found
```
**Causa:** GraphicsMagick não está instalado no sistema/container.
**Solução:**
**Docker:**
```bash
# Rebuild a imagem Docker com GraphicsMagick incluído
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
**Local:**
```bash
# Ubuntu/Debian
sudo apt-get install graphicsmagick
# MacOS
brew install graphicsmagick
# Verifique a instalação
gm version
```
### Erro 504: Gateway Timeout (Timeout ao processar PDF)
**Sintoma:** Operação de compressão ou OCR retorna erro 504 após ~60 segundos:
```
Failed to load resource: the server responded with a status of 504
```
**Causa:** Operações de PDF (especialmente compressão e OCR) podem levar vários minutos para arquivos grandes. O timeout padrão de 60 segundos é insuficiente.
**Solução:** As configurações de timeout foram aumentadas para **10 minutos** em:
- ✅ Express server (`src/server/index.ts`)
- ✅ Vite proxy (`vite.config.ts`)
- ✅ Axios frontend (`PDFManipulationPage.tsx`)
Se ainda ocorrer timeout:
1. Verifique se rebuild foi feito após as alterações
2. Reduza o tamanho do arquivo ou qualidade
3. Processe menos arquivos por vez
4. Para PDFs muito grandes (>50MB), considere fazer upload em partes
**Dicas de performance:**
- Compressão: Use qualidade 50-70 para PDFs grandes
- OCR: Processe apenas páginas necessárias, não o PDF inteiro
- Split: Use chunks menores para PDFs com muitas páginas
### Erro: ENOENT ao deletar arquivos do histórico
**Sintoma:** Erro ao limpar histórico:
```
Error: ENOENT: no such file or directory, unlink '/path/to/file.pdf'
```
**Causa:** Tentativa de deletar arquivos que não existem mais (já foram removidos manualmente ou por outro processo).
**Solução:** ✅ **Corrigido automaticamente** - O sistema agora verifica se o arquivo existe antes de tentar deletar e ignora erros ENOENT silenciosamente.
### Erro: Imagens baixadas com extensão .pdf (ou sem extensão correta)
**Sintoma:** Ao baixar imagens geradas pela conversão PDF→Imagens, os arquivos vêm com extensão .pdf ou precisam ser renomeados para .png/.jpg para serem visualizados.
**Causa:** O servidor não estava enviando o Content-Type correto no header HTTP, OU o frontend estava ignorando os headers do servidor e forçando o nome como `.pdf`.
**Solução:** ✅ **Corrigido automaticamente** - O sistema agora:
**Backend:**
- Detecta automaticamente o tipo MIME correto baseado na extensão (.png, .jpg, .webp, .zip, .pdf)
- Define headers HTTP apropriados (`Content-Type` e `Content-Disposition`)
- Garante que o nome do arquivo no download mantém a extensão original
- Adiciona logs de debug para facilitar troubleshooting
**Frontend:**
- Extrai o nome do arquivo do header `Content-Disposition` enviado pelo servidor
- Usa o `Content-Type` correto do servidor ao criar o Blob para download
- Não força mais extensão `.pdf` em todos os downloads
- Adiciona log no console do browser: `Download frontend: arquivo.png (image/png)`
- Libera memória do blob URL após download
Se ainda ocorrer o problema:
1. Abra o Console do DevTools (F12) e verifique os logs de download
2. Verifique se os headers estão sendo recebidos corretamente na aba Network
3. Limpe o cache do browser (Ctrl+Shift+Del)
4. Teste em uma aba anônima/privada
5. Use a opção de **Download ZIP** que sempre funciona corretamente
### Erro: "Tesseract worker failed"
Solução: Certifique-se de que o Tesseract está instalado corretamente:
```bash
npm install tesseract.js --force
```
### Erro: "Cannot find module 'sharp'"
Solução: Reinstale o Sharp com rebuild:
```bash
npm uninstall sharp
npm install sharp
npm rebuild sharp
```
### Erro: "ENOSPC: no space left on device"
Solução: Aumente o espaço em disco ou execute a limpeza:
```bash
npm run cleanup-pdf-files
```
### Performance lenta no OCR
Solução: Reduza o DPI ou processe menos páginas por vez.
## Suporte
Para questões ou problemas:
1. Verifique os logs do servidor
2. Consulte a tabela `operacoes_pdf` para detalhes do erro
3. Verifique se os diretórios de storage têm permissões corretas
4. Certifique-se de que todas as dependências estão instaladas
## Changelog
### v1.1.2 (2026-02-27)
- 🔧 **Correção DEFINITIVA de downloads** - Arquivos agora baixam com nome e extensão corretos
- **Backend:** Detecta tipo MIME e define headers HTTP apropriados (Content-Type, Content-Disposition)
- **Frontend:** Extrai nome do arquivo e tipo MIME dos headers da resposta
- **Frontend:** Não força mais `.pdf` em todos os downloads - usa nome real do servidor
- **Frontend:** Cria Blob com Content-Type correto preservando o formato original
- Adiciona logs de debug no backend e frontend para troubleshooting
- Suporta formato 'jpeg' além de 'jpg'
- Fallback para PNG em formatos não suportados
- Libera memória de blob URLs após download
- 🐛 **Melhorias no pdfToImages** - Adicionado suporte para 'jpeg' e fallback para formatos inválidos
### v1.1.1 (2026-02-27)
- 🔧 **Correção de timeout 504** - Aumentado timeout para 10 minutos em todas as camadas
- Express server: 600 segundos
- Vite proxy: 600 segundos
- Axios frontend: 600 segundos
- 🗑️ **Correção ENOENT** - Sistema agora ignora arquivos já deletados ao limpar histórico
- 📋 **Gerenciamento de histórico** - Botões para limpar histórico completo ou arquivos antigos
- 🐛 **Melhorias de estabilidade** - Verificação de existência de arquivos antes de deletar
- 📚 **Documentação expandida** - Adicionado troubleshooting completo
### v1.1.0 (2026-02-27)
- 🚀 **Compressão agressiva** com redução >60% do tamanho original
- 📦 **Download em ZIP** para operações com múltiplos arquivos (split, pdfToImages)
- 🔍 **OCR embutido** - opção de tornar PDFs pesquisáveis (compress, merge, imagesToPDF)
- ⚡ **Otimização de imagens** com qualidade ajustável (30-100%)
- 🎯 **Interface aprimorada** com indicadores visuais e opções de OCR
### v1.0.0 (2026-02-27)
- ✅ Implementação inicial
- ✅ Compressão sem perda de qualidade
- ✅ Divisão de PDFs
- ✅ Mesclagem de PDFs
- ✅ Conversão PDF para imagens
- ✅ Conversão de imagens para PDF
- ✅ OCR com Tesseract
- ✅ Histórico de operações
- ✅ Interface React completa
- ✅ Validação e segurança
+611 -1715
View File
File diff suppressed because it is too large. Load diff
+19 -21
View File
@@ -15,12 +15,13 @@
"postinstall": "prisma generate && npx puppeteer browsers install chrome", "postinstall": "prisma generate && npx puppeteer browsers install chrome",
"server": "ts-node --transpile-only src/server/index.ts", "server": "ts-node --transpile-only src/server/index.ts",
"dev:full": "concurrently -n \"frontend,backend\" -c \"blue,green\" \"npm run dev\" \"npm run server\"", "dev:full": "concurrently -n \"frontend,backend\" -c \"blue,green\" \"npm run dev\" \"npm run server\"",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:fix": "eslint . --ext .ts,.tsx --fix",
"create-admin": "ts-node scripts/create_admin_user.ts", "create-admin": "ts-node scripts/create_admin_user.ts",
"worker": "ts-node src/workers/startWorker.ts", "worker": "ts-node src/workers/startWorker.ts",
"install-deps": "npm install ua-parser-js uuid axios xlsx", "install-deps": "npm install ua-parser-js uuid axios xlsx",
"security-check": "npm audit" "install-pdf-deps": "npm install pdf-parse pdf2pic tesseract.js archiver && npm install --save-dev @types/pdf-parse @types/archiver",
"cleanup-pdf-files": "ts-node scripts/cleanup_pdf_files.ts"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
@@ -37,7 +38,8 @@
"@tiptap/starter-kit": "^2.11.7", "@tiptap/starter-kit": "^2.11.7",
"@types/sharp": "^0.32.0", "@types/sharp": "^0.32.0",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"axios": "^1.8.4", "archiver": "^7.0.1",
"axios": "^1.6.5",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"bullmq": "^4.12.0", "bullmq": "^4.12.0",
@@ -45,14 +47,17 @@
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"detect-file-type": "^0.2.3", "detect-file-type": "^0.2.3",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"exceljs": "^4.4.0",
"express": "^4.18.2", "express": "^4.18.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"opentimestamps-javascript": "^1.0.1", "opentimestamps-javascript": "^1.0.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdf-parse": "^1.1.1",
"pdf2pic": "^3.1.3",
"pdfkit": "^0.13.0",
"puppeteer": "^24.5.0", "puppeteer": "^24.5.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"tesseract.js": "^5.0.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -64,36 +69,35 @@
"react-toastify": "^10.0.4", "react-toastify": "^10.0.4",
"redis": "^4.6.10", "redis": "^4.6.10",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"tough-cookie": "^4.1.3",
"ua-parser-js": "^1.0.35", "ua-parser-js": "^1.0.35",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"webdav": "^5.3.1", "webdav": "^5.3.1",
"xlsx": "^0.18.5",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.1", "@tailwindcss/postcss": "^4.1.1",
"@types/archiver": "^6.0.2",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@types/pdf-parse": "^1.1.4",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.29.1", "@typescript-eslint/parser": "^8.28.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^8.57.0", "eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.2", "eslint-config-prettier": "^10.1.1",
"eslint-import-resolver-typescript": "^4.3.2", "eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@@ -116,11 +120,5 @@
"--disable-gpu", "--disable-gpu",
"--window-size=1920x1080" "--window-size=1920x1080"
] ]
},
"puppeteerOptions": {
"browserFetcher": {
"platform": "win32",
"downloadPath": ".cache/puppeteer"
}
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
"tailwindcss/nesting": {}, 'tailwindcss/nesting': {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; }
@@ -0,0 +1,36 @@
-- Migration para adicionar tabela de operações de PDF
-- Created: 2026-02-27
-- Criar ENUM para tipo de operação
CREATE TYPE "TipoOperacaoPDF" AS ENUM ('COMPRIMIR', 'DIVIDIR', 'MESCLAR', 'PDF_PARA_IMAGEM', 'IMAGEM_PARA_PDF', 'OCR');
-- Criar ENUM para status da operação
CREATE TYPE "StatusOperacaoPDF" AS ENUM ('PROCESSANDO', 'CONCLUIDO', 'ERRO');
-- Criar tabela de operações de PDF
CREATE TABLE "operacoes_pdf" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tipoOperacao" "TipoOperacaoPDF" NOT NULL,
"status" "StatusOperacaoPDF" NOT NULL DEFAULT 'PROCESSANDO',
"usuarioId" UUID NOT NULL,
"arquivosOriginais" TEXT[] NOT NULL,
"arquivosResultado" TEXT[] NOT NULL,
"tamanhoOriginal" BIGINT NOT NULL,
"tamanhoResultado" BIGINT,
"parametros" JSONB,
"mensagemErro" TEXT,
"detalhesOCR" JSONB,
"tempoProcessamento" INTEGER,
"criadoEm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"atualizadoEm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "operacoes_pdf_pkey" PRIMARY KEY ("id")
);
-- Adicionar foreign key para usuarios
ALTER TABLE "operacoes_pdf" ADD CONSTRAINT "operacoes_pdf_usuarioId_fkey" FOREIGN KEY ("usuarioId") REFERENCES "usuarios"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Criar índice para melhor performance nas consultas
CREATE INDEX "operacoes_pdf_usuarioId_idx" ON "operacoes_pdf"("usuarioId");
CREATE INDEX "operacoes_pdf_criadoEm_idx" ON "operacoes_pdf"("criadoEm" DESC);
CREATE INDEX "operacoes_pdf_status_idx" ON "operacoes_pdf"("status");
@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient(); const prisma = new PrismaClient()
async function main() { async function main() {
await prisma.$executeRaw` await prisma.$executeRaw`
@@ -43,7 +43,7 @@ async function main() {
} }
main() main()
.catch((e) => { .catch(e => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient(); const prisma = new PrismaClient()
async function main() { async function main() {
await prisma.$executeRaw` await prisma.$executeRaw`
@@ -10,7 +10,7 @@ async function main() {
} }
main() main()
.catch((e) => { .catch(e => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient(); const prisma = new PrismaClient()
async function main() { async function main() {
await prisma.$executeRaw` await prisma.$executeRaw`
@@ -13,7 +13,7 @@ async function main() {
} }
main() main()
.catch((e) => { .catch(e => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient(); const prisma = new PrismaClient()
async function main() { async function main() {
await prisma.$executeRaw` await prisma.$executeRaw`
@@ -17,15 +17,15 @@ async function main() {
// Inserir configuração padrão // Inserir configuração padrão
await prisma.configuracaoDocumento.create({ await prisma.configuracaoDocumento.create({
data: { data: {
headerImageUrl: "", headerImageUrl: '',
footerImageUrl: "", footerImageUrl: '',
logoUrl: "", logoUrl: ''
}, }
}); });
} }
main() main()
.catch((e) => { .catch(e => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient(); const prisma = new PrismaClient()
async function main() { async function main() {
await prisma.$executeRaw` await prisma.$executeRaw`
@@ -13,7 +13,7 @@ async function main() {
} }
main() main()
.catch((e) => { .catch(e => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
+47
View File
@@ -51,6 +51,9 @@ model Usuario {
// Adicionar relação para cálculos de pensão alimentícia // Adicionar relação para cálculos de pensão alimentícia
calculosPensaoAlimenticia CalculoPensaoAlimenticia[] calculosPensaoAlimenticia CalculoPensaoAlimenticia[]
// Adicionar relação para operações de PDF
operacoesPDF OperacaoPDF[]
@@map("usuarios") @@map("usuarios")
} }
@@ -1067,3 +1070,47 @@ model CalculoPensaoAlimenticia {
@@map("calculos_pensao_alimenticia") @@map("calculos_pensao_alimenticia")
} }
// Modelo para histórico de operações de PDF
enum TipoOperacaoPDF {
COMPRIMIR
DIVIDIR
MESCLAR
PDF_PARA_IMAGEM
IMAGEM_PARA_PDF
OCR
}
enum StatusOperacaoPDF {
PROCESSANDO
CONCLUIDO
ERRO
}
model OperacaoPDF {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
tipoOperacao TipoOperacaoPDF
status StatusOperacaoPDF @default(PROCESSANDO)
usuarioId String @db.Uuid
usuario Usuario @relation(fields: [usuarioId], references: [id])
// Informações dos arquivos
arquivosOriginais String[] // Array de paths dos arquivos originais
arquivosResultado String[] // Array de paths dos arquivos resultantes
tamanhoOriginal BigInt // Tamanho total dos arquivos originais em bytes
tamanhoResultado BigInt? // Tamanho total dos arquivos resultantes em bytes
// Parâmetros da operação
parametros Json? // Parâmetros específicos da operação (ex: páginas para dividir)
// Resultado da operação
mensagemErro String?
detalhesOCR Json? // Quando for OCR, armazena o texto extraído
tempoProcessamento Int? // Tempo de processamento em milissegundos
criadoEm DateTime @default(now())
atualizadoEm DateTime @updatedAt
@@map("operacoes_pdf")
}
+120 -127
View File
@@ -1,223 +1,216 @@
tinymce.addI18n("pt_BR", {
Cut: "Recortar", tinymce.addI18n('pt_BR',{
"Cut": "Recortar",
"Heading 5": "Cabe\u00e7alho 5", "Heading 5": "Cabe\u00e7alho 5",
"Header 2": "Cabe\u00e7alho 2", "Header 2": "Cabe\u00e7alho 2",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Seu navegador n\u00e3o suporta acesso direto \u00e0 \u00e1rea de transfer\u00eancia. Por favor use os atalhos Ctrl+X - C - V do teclado",
"Seu navegador n\u00e3o suporta acesso direto \u00e0 \u00e1rea de transfer\u00eancia. Por favor use os atalhos Ctrl+X - C - V do teclado",
"Heading 4": "Cabe\u00e7alho 4", "Heading 4": "Cabe\u00e7alho 4",
Div: "Div", "Div": "Div",
"Heading 2": "Cabe\u00e7alho 2", "Heading 2": "Cabe\u00e7alho 2",
Paste: "Colar", "Paste": "Colar",
Close: "Fechar", "Close": "Fechar",
"Font Family": "Fonte", "Font Family": "Fonte",
Pre: "Pre", "Pre": "Pre",
"Align right": "Alinhar \u00e0 direita", "Align right": "Alinhar \u00e0 direita",
"New document": "Novo documento", "New document": "Novo documento",
Blockquote: "Aspas", "Blockquote": "Aspas",
"Numbered list": "Lista ordenada", "Numbered list": "Lista ordenada",
"Heading 1": "Cabe\u00e7alho 1", "Heading 1": "Cabe\u00e7alho 1",
Headings: "Cabe\u00e7alhos", "Headings": "Cabe\u00e7alhos",
"Increase indent": "Aumentar recuo", "Increase indent": "Aumentar recuo",
Formats: "Formatos", "Formats": "Formatos",
Headers: "Cabe\u00e7alhos", "Headers": "Cabe\u00e7alhos",
"Select all": "Selecionar tudo", "Select all": "Selecionar tudo",
"Header 3": "Cabe\u00e7alho 3", "Header 3": "Cabe\u00e7alho 3",
Blocks: "Blocos", "Blocks": "Blocos",
Undo: "Desfazer", "Undo": "Desfazer",
Strikethrough: "Riscar", "Strikethrough": "Riscar",
"Bullet list": "Lista n\u00e3o ordenada", "Bullet list": "Lista n\u00e3o ordenada",
"Header 1": "Cabe\u00e7alho 1", "Header 1": "Cabe\u00e7alho 1",
Superscript: "Sobrescrito", "Superscript": "Sobrescrito",
"Clear formatting": "Limpar formata\u00e7\u00e3o", "Clear formatting": "Limpar formata\u00e7\u00e3o",
"Font Sizes": "Tamanho", "Font Sizes": "Tamanho",
Subscript: "Subscrever", "Subscript": "Subscrever",
"Header 6": "Cabe\u00e7alho 6", "Header 6": "Cabe\u00e7alho 6",
Redo: "Refazer", "Redo": "Refazer",
Paragraph: "Par\u00e1grafo", "Paragraph": "Par\u00e1grafo",
Ok: "Ok", "Ok": "Ok",
Bold: "Negrito", "Bold": "Negrito",
Code: "C\u00f3digo", "Code": "C\u00f3digo",
Italic: "It\u00e1lico", "Italic": "It\u00e1lico",
"Align center": "Centralizar", "Align center": "Centralizar",
"Header 5": "Cabe\u00e7alho 5", "Header 5": "Cabe\u00e7alho 5",
"Heading 6": "Cabe\u00e7alho 6", "Heading 6": "Cabe\u00e7alho 6",
"Heading 3": "Cabe\u00e7alho 3", "Heading 3": "Cabe\u00e7alho 3",
"Decrease indent": "Diminuir recuo", "Decrease indent": "Diminuir recuo",
"Header 4": "Cabe\u00e7alho 4", "Header 4": "Cabe\u00e7alho 4",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "O comando colar est\u00e1 agora em modo texto plano. O conte\u00fado ser\u00e1 colado como texto plano at\u00e9 voc\u00ea desligar esta op\u00e7\u00e3o.",
"O comando colar est\u00e1 agora em modo texto plano. O conte\u00fado ser\u00e1 colado como texto plano at\u00e9 voc\u00ea desligar esta op\u00e7\u00e3o.", "Underline": "Sublinhar",
Underline: "Sublinhar", "Cancel": "Cancelar",
Cancel: "Cancelar", "Justify": "Justificar",
Justify: "Justificar", "Inline": "Em linha",
Inline: "Em linha", "Copy": "Copiar",
Copy: "Copiar",
"Align left": "Alinhar \u00e0 esquerda", "Align left": "Alinhar \u00e0 esquerda",
"Visual aids": "Ajuda visual", "Visual aids": "Ajuda visual",
"Lower Greek": "\u03b1. \u03b2. \u03b3. ...", "Lower Greek": "\u03b1. \u03b2. \u03b3. ...",
Square: "Quadrado", "Square": "Quadrado",
Default: "Padr\u00e3o", "Default": "Padr\u00e3o",
"Lower Alpha": "a. b. c. ...", "Lower Alpha": "a. b. c. ...",
Circle: "C\u00edrculo", "Circle": "C\u00edrculo",
Disc: "Disco", "Disc": "Disco",
"Upper Alpha": "A. B. C. ...", "Upper Alpha": "A. B. C. ...",
"Upper Roman": "I. II. III. ...", "Upper Roman": "I. II. III. ...",
"Lower Roman": "i. ii. iii. ...", "Lower Roman": "i. ii. iii. ...",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Id deve come\u00e7ar com uma letra, seguido apenas por letras, n\u00fameros, tra\u00e7os, pontos, dois pontos ou sublinhados.",
"Id deve come\u00e7ar com uma letra, seguido apenas por letras, n\u00fameros, tra\u00e7os, pontos, dois pontos ou sublinhados.", "Name": "Nome",
Name: "Nome", "Anchor": "\u00c2ncora",
Anchor: "\u00c2ncora", "Id": "Id",
Id: "Id", "You have unsaved changes are you sure you want to navigate away?": "Voc\u00ea tem mudan\u00e7as n\u00e3o salvas. Voc\u00ea tem certeza que deseja sair?",
"You have unsaved changes are you sure you want to navigate away?":
"Voc\u00ea tem mudan\u00e7as n\u00e3o salvas. Voc\u00ea tem certeza que deseja sair?",
"Restore last draft": "Restaurar \u00faltimo rascunho", "Restore last draft": "Restaurar \u00faltimo rascunho",
"Special character": "Caracteres especiais", "Special character": "Caracteres especiais",
"Source code": "C\u00f3digo fonte", "Source code": "C\u00f3digo fonte",
Language: "Idioma", "Language": "Idioma",
"Insert\/Edit code sample": "Inserir\/Editar c\u00f3digo de exemplo", "Insert\/Edit code sample": "Inserir\/Editar c\u00f3digo de exemplo",
B: "B", "B": "B",
R: "R", "R": "R",
G: "G", "G": "G",
Color: "Cor", "Color": "Cor",
"Right to left": "Da direita para a esquerda", "Right to left": "Da direita para a esquerda",
"Left to right": "Da esquerda para a direita", "Left to right": "Da esquerda para a direita",
Emoticons: "Emoticons", "Emoticons": "Emoticons",
Robots: "Rob\u00f4s", "Robots": "Rob\u00f4s",
"Document properties": "Propriedades do documento", "Document properties": "Propriedades do documento",
Title: "T\u00edtulo", "Title": "T\u00edtulo",
Keywords: "Palavras-chave", "Keywords": "Palavras-chave",
Encoding: "Codifica\u00e7\u00e3o", "Encoding": "Codifica\u00e7\u00e3o",
Description: "Descri\u00e7\u00e3o", "Description": "Descri\u00e7\u00e3o",
Author: "Autor", "Author": "Autor",
Fullscreen: "Tela cheia", "Fullscreen": "Tela cheia",
"Horizontal line": "Linha horizontal", "Horizontal line": "Linha horizontal",
"Horizontal space": "Espa\u00e7amento horizontal", "Horizontal space": "Espa\u00e7amento horizontal",
"Insert\/edit image": "Inserir\/editar imagem", "Insert\/edit image": "Inserir\/editar imagem",
General: "Geral", "General": "Geral",
Advanced: "Avan\u00e7ado", "Advanced": "Avan\u00e7ado",
Source: "Endere\u00e7o da imagem", "Source": "Endere\u00e7o da imagem",
Border: "Borda", "Border": "Borda",
"Constrain proportions": "Manter propor\u00e7\u00f5es", "Constrain proportions": "Manter propor\u00e7\u00f5es",
"Vertical space": "Espa\u00e7amento vertical", "Vertical space": "Espa\u00e7amento vertical",
"Image description": "Inserir descri\u00e7\u00e3o", "Image description": "Inserir descri\u00e7\u00e3o",
Style: "Estilo", "Style": "Estilo",
Dimensions: "Dimens\u00f5es", "Dimensions": "Dimens\u00f5es",
"Insert image": "Inserir imagem", "Insert image": "Inserir imagem",
Image: "Imagem", "Image": "Imagem",
"Zoom in": "Aumentar zoom", "Zoom in": "Aumentar zoom",
Contrast: "Contraste", "Contrast": "Contraste",
Back: "Voltar", "Back": "Voltar",
Gamma: "Gama", "Gamma": "Gama",
"Flip horizontally": "Virar horizontalmente", "Flip horizontally": "Virar horizontalmente",
Resize: "Redimensionar", "Resize": "Redimensionar",
Sharpen: "Aumentar nitidez", "Sharpen": "Aumentar nitidez",
"Zoom out": "Diminuir zoom", "Zoom out": "Diminuir zoom",
"Image options": "Op\u00e7\u00f5es de Imagem", "Image options": "Op\u00e7\u00f5es de Imagem",
Apply: "Aplicar", "Apply": "Aplicar",
Brightness: "Brilho", "Brightness": "Brilho",
"Rotate clockwise": "Girar em sentido anti-hor\u00e1rio", "Rotate clockwise": "Girar em sentido anti-hor\u00e1rio",
"Rotate counterclockwise": "Girar em sentido hor\u00e1rio", "Rotate counterclockwise": "Girar em sentido hor\u00e1rio",
"Edit image": "Editar imagem", "Edit image": "Editar imagem",
"Color levels": "N\u00edveis de cor", "Color levels": "N\u00edveis de cor",
Crop: "Cortar", "Crop": "Cortar",
Orientation: "Orienta\u00e7\u00e3o", "Orientation": "Orienta\u00e7\u00e3o",
"Flip vertically": "Virar verticalmente", "Flip vertically": "Virar verticalmente",
Invert: "Inverter", "Invert": "Inverter",
"Date\/time": "data\/hora", "Date\/time": "data\/hora",
"Insert date\/time": "Inserir data\/hora", "Insert date\/time": "Inserir data\/hora",
"Remove link": "Remover link", "Remove link": "Remover link",
Url: "Url", "Url": "Url",
"Text to display": "Texto para mostrar", "Text to display": "Texto para mostrar",
Anchors: "\u00c2ncoras", "Anchors": "\u00c2ncoras",
"Insert link": "Inserir link", "Insert link": "Inserir link",
Link: "Link", "Link": "Link",
"New window": "Nova janela", "New window": "Nova janela",
None: "Nenhum", "None": "Nenhum",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "A URL que voc\u00ea informou parece ser um link externo. Deseja incluir o prefixo http:\/\/?",
"A URL que voc\u00ea informou parece ser um link externo. Deseja incluir o prefixo http:\/\/?",
"Paste or type a link": "Cole ou digite um Link", "Paste or type a link": "Cole ou digite um Link",
Target: "Alvo", "Target": "Alvo",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?",
"Insert\/edit link": "Inserir\/editar link", "Insert\/edit link": "Inserir\/editar link",
"Insert\/edit video": "Inserir\/editar v\u00eddeo", "Insert\/edit video": "Inserir\/editar v\u00eddeo",
Media: "imagem", "Media": "imagem",
"Alternative source": "Fonte alternativa", "Alternative source": "Fonte alternativa",
"Paste your embed code below:": "Paste your embed code below:": "Insira o c\u00f3digo de incorpora\u00e7\u00e3o abaixo:",
"Insira o c\u00f3digo de incorpora\u00e7\u00e3o abaixo:",
"Insert video": "Inserir v\u00eddeo", "Insert video": "Inserir v\u00eddeo",
Poster: "Autor", "Poster": "Autor",
"Insert\/edit media": "Inserir\/editar imagem", "Insert\/edit media": "Inserir\/editar imagem",
Embed: "Incorporar", "Embed": "Incorporar",
"Nonbreaking space": "Espa\u00e7o n\u00e3o separ\u00e1vel", "Nonbreaking space": "Espa\u00e7o n\u00e3o separ\u00e1vel",
"Page break": "Quebra de p\u00e1gina", "Page break": "Quebra de p\u00e1gina",
"Paste as text": "Colar como texto", "Paste as text": "Colar como texto",
Preview: "Pr\u00e9-visualizar", "Preview": "Pr\u00e9-visualizar",
Print: "Imprimir", "Print": "Imprimir",
Save: "Salvar", "Save": "Salvar",
"Could not find the specified string.": "Could not find the specified string.": "N\u00e3o foi poss\u00edvel encontrar o termo especificado",
"N\u00e3o foi poss\u00edvel encontrar o termo especificado", "Replace": "Substituir",
Replace: "Substituir", "Next": "Pr\u00f3ximo",
Next: "Pr\u00f3ximo",
"Whole words": "Palavras inteiras", "Whole words": "Palavras inteiras",
"Find and replace": "Localizar e substituir", "Find and replace": "Localizar e substituir",
"Replace with": "Substituir por", "Replace with": "Substituir por",
Find: "Localizar", "Find": "Localizar",
"Replace all": "Substituir tudo", "Replace all": "Substituir tudo",
"Match case": "Diferenciar mai\u00fasculas e min\u00fasculas", "Match case": "Diferenciar mai\u00fasculas e min\u00fasculas",
Prev: "Anterior", "Prev": "Anterior",
Spellcheck: "Corretor ortogr\u00e1fico", "Spellcheck": "Corretor ortogr\u00e1fico",
Finish: "Finalizar", "Finish": "Finalizar",
"Ignore all": "Ignorar tudo", "Ignore all": "Ignorar tudo",
Ignore: "Ignorar", "Ignore": "Ignorar",
"Add to Dictionary": "Adicionar ao Dicion\u00e1rio", "Add to Dictionary": "Adicionar ao Dicion\u00e1rio",
"Insert row before": "Inserir linha antes", "Insert row before": "Inserir linha antes",
Rows: "Linhas", "Rows": "Linhas",
Height: "Altura", "Height": "Altura",
"Paste row after": "Colar linha depois", "Paste row after": "Colar linha depois",
Alignment: "Alinhamento", "Alignment": "Alinhamento",
"Border color": "Cor da borda", "Border color": "Cor da borda",
"Column group": "Agrupar coluna", "Column group": "Agrupar coluna",
Row: "Linha", "Row": "Linha",
"Insert column before": "Inserir coluna antes", "Insert column before": "Inserir coluna antes",
"Split cell": "Dividir c\u00e9lula", "Split cell": "Dividir c\u00e9lula",
"Cell padding": "Espa\u00e7amento interno da c\u00e9lula", "Cell padding": "Espa\u00e7amento interno da c\u00e9lula",
"Cell spacing": "Espa\u00e7amento da c\u00e9lula", "Cell spacing": "Espa\u00e7amento da c\u00e9lula",
"Row type": "Tipo de linha", "Row type": "Tipo de linha",
"Insert table": "Inserir tabela", "Insert table": "Inserir tabela",
Body: "Corpo", "Body": "Corpo",
Caption: "Legenda", "Caption": "Legenda",
Footer: "Rodap\u00e9", "Footer": "Rodap\u00e9",
"Delete row": "Excluir linha", "Delete row": "Excluir linha",
"Paste row before": "Colar linha antes", "Paste row before": "Colar linha antes",
Scope: "Escopo", "Scope": "Escopo",
"Delete table": "Excluir tabela", "Delete table": "Excluir tabela",
"H Align": "Alinhamento H", "H Align": "Alinhamento H",
Top: "Superior", "Top": "Superior",
"Header cell": "C\u00e9lula cabe\u00e7alho", "Header cell": "C\u00e9lula cabe\u00e7alho",
Column: "Coluna", "Column": "Coluna",
"Row group": "Agrupar linha", "Row group": "Agrupar linha",
Cell: "C\u00e9lula", "Cell": "C\u00e9lula",
Middle: "Meio", "Middle": "Meio",
"Cell type": "Tipo de c\u00e9lula", "Cell type": "Tipo de c\u00e9lula",
"Copy row": "Copiar linha", "Copy row": "Copiar linha",
"Row properties": "Propriedades da linha", "Row properties": "Propriedades da linha",
"Table properties": "Propriedades da tabela", "Table properties": "Propriedades da tabela",
Bottom: "Inferior", "Bottom": "Inferior",
"V Align": "Alinhamento V", "V Align": "Alinhamento V",
Header: "Cabe\u00e7alho", "Header": "Cabe\u00e7alho",
Right: "Direita", "Right": "Direita",
"Insert column after": "Inserir coluna depois", "Insert column after": "Inserir coluna depois",
Cols: "Colunas", "Cols": "Colunas",
"Insert row after": "Inserir linha depois", "Insert row after": "Inserir linha depois",
Width: "Largura", "Width": "Largura",
"Cell properties": "Propriedades da c\u00e9lula", "Cell properties": "Propriedades da c\u00e9lula",
Left: "Esquerdo", "Left": "Esquerdo",
"Cut row": "Recortar linha", "Cut row": "Recortar linha",
"Delete column": "Excluir coluna", "Delete column": "Excluir coluna",
Center: "Centro", "Center": "Centro",
"Merge cells": "Agrupar c\u00e9lulas", "Merge cells": "Agrupar c\u00e9lulas",
"Insert template": "Inserir modelo", "Insert template": "Inserir modelo",
Templates: "Modelos", "Templates": "Modelos",
"Background color": "Cor do fundo", "Background color": "Cor do fundo",
"Custom...": "Personalizado...", "Custom...": "Personalizado...",
"Custom color": "Cor personalizada", "Custom color": "Cor personalizada",
@@ -227,13 +220,13 @@ tinymce.addI18n("pt_BR", {
"Show blocks": "Mostrar blocos", "Show blocks": "Mostrar blocos",
"Show invisible characters": "Exibir caracteres invis\u00edveis", "Show invisible characters": "Exibir caracteres invis\u00edveis",
"Words: {0}": "Palavras: {0}", "Words: {0}": "Palavras: {0}",
Insert: "Inserir", "Insert": "Inserir",
File: "Arquivo", "File": "Arquivo",
Edit: "Editar", "Edit": "Editar",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u00c1rea de texto formatado. Pressione ALT-F9 para exibir o menu, ALT-F10 para exibir a barra de ferramentas ou ALT-0 para exibir a ajuda",
"\u00c1rea de texto formatado. Pressione ALT-F9 para exibir o menu, ALT-F10 para exibir a barra de ferramentas ou ALT-0 para exibir a ajuda", "Tools": "Ferramentas",
Tools: "Ferramentas", "View": "Visualizar",
View: "Visualizar", "Table": "Tabela",
Table: "Tabela", "Format": "Formatar"
Format: "Formatar", });
});
+41
View File
@@ -0,0 +1,41 @@
import { PDFService } from '../src/services/pdfService';
/**
* Script para limpeza de arquivos PDF antigos
* Remove arquivos com mais de 24 horas por padrão
*
* Uso:
* npm run cleanup-pdf-files
*
* Ou via ts-node:
* ts-node scripts/cleanup_pdf_files.ts [horas]
*/
async function main() {
console.log('Iniciando limpeza de arquivos PDF antigos...');
const horasArg = process.argv[2];
const horas = horasArg ? parseInt(horasArg) : 24;
if (isNaN(horas) || horas < 0) {
console.error('Erro: O argumento deve ser um número positivo de horas');
process.exit(1);
}
console.log(`Removendo arquivos com mais de ${horas} horas...`);
try {
await PDFService.cleanupOldFiles(horas);
console.log('✅ Limpeza concluída com sucesso!');
} catch (error) {
console.error('❌ Erro durante a limpeza:', error);
process.exit(1);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error('Erro fatal:', error);
process.exit(1);
});
+5 -4
View File
@@ -16,27 +16,27 @@ async function createAdminUser() {
ativo: true, ativo: true,
cpf: "06116321100", cpf: "06116321100",
oab: "12774", oab: "12774",
oabUF: "TO", oabUF: "TO"
}; };
console.log("Tentando criar/atualizar usuário admin..."); console.log("Tentando criar/atualizar usuário admin...");
// Verifica se já existe na tabela usuarios // Verifica se já existe na tabela usuarios
const existingAdmin = await prisma.usuario.findUnique({ const existingAdmin = await prisma.usuario.findUnique({
where: { email: adminData.email }, where: { email: adminData.email }
}); });
if (existingAdmin) { if (existingAdmin) {
console.log("Usuário admin já existe. Atualizando..."); console.log("Usuário admin já existe. Atualizando...");
await prisma.usuario.update({ await prisma.usuario.update({
where: { email: adminData.email }, where: { email: adminData.email },
data: adminData, data: adminData
}); });
console.log("Admin atualizado com sucesso!"); console.log("Admin atualizado com sucesso!");
} else { } else {
console.log("Criando novo usuário admin..."); console.log("Criando novo usuário admin...");
await prisma.usuario.create({ await prisma.usuario.create({
data: adminData, data: adminData
}); });
console.log("Novo admin criado com sucesso!"); console.log("Novo admin criado com sucesso!");
} }
@@ -44,6 +44,7 @@ async function createAdminUser() {
console.log("\nCredenciais de acesso:"); console.log("\nCredenciais de acesso:");
console.log("Email:", adminData.email); console.log("Email:", adminData.email);
console.log("Senha: 34641747"); console.log("Senha: 34641747");
} catch (error) { } catch (error) {
console.error("Erro ao criar/atualizar usuário admin:", error); console.error("Erro ao criar/atualizar usuário admin:", error);
} finally { } finally {
+9
View File
@@ -0,0 +1,9 @@
-- Identificar registros com UUID inválido
SELECT id, uuid_column
FROM atendimento
WHERE uuid_column !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
-- Atualizar registros com UUID inválido
UPDATE atendimento
SET uuid_column = gen_random_uuid()
WHERE uuid_column !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
+37
View File
@@ -0,0 +1,37 @@
@echo off
:: Script para rebuild completo do Docker com dependências PDF
:: Use este script após atualizar o Dockerfile para incluir GraphicsMagick
echo ========================================
echo Rebuild Docker - Sistema PDF
echo ========================================
echo.
echo Parando containers...
docker-compose down
echo.
echo Removendo imagens antigas...
docker image prune -f
echo.
echo Reconstruindo imagem (sem cache)...
docker-compose build --no-cache
echo.
echo Iniciando containers...
docker-compose up -d
echo.
echo ========================================
echo Rebuild concluído!
echo ========================================
echo.
echo Verificando status dos containers:
docker-compose ps
echo.
echo Para ver os logs:
echo docker-compose logs -f backend
echo.
pause
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Script para rebuild completo do Docker com dependências PDF
# Use este script após atualizar o Dockerfile para incluir GraphicsMagick
echo "========================================"
echo "Rebuild Docker - Sistema PDF"
echo "========================================"
echo ""
echo "Parando containers..."
docker-compose down
echo ""
echo "Removendo imagens antigas..."
docker image prune -f
echo ""
echo "Reconstruindo imagem (sem cache)..."
docker-compose build --no-cache
echo ""
echo "Iniciando containers..."
docker-compose up -d
echo ""
echo "========================================"
echo "Rebuild concluído!"
echo "========================================"
echo ""
echo "Verificando status dos containers:"
docker-compose ps
echo ""
echo "Para ver os logs:"
echo " docker-compose logs -f backend"
echo ""
+271
View File
@@ -0,0 +1,271 @@
import { PDFService } from '../src/services/pdfService';
import path from 'path';
import fs from 'fs/promises';
/**
* Script de teste para funcionalidades de PDF
*
* ATENÇÃO: Este é um script de teste. Certifique-se de ter arquivos de teste
* disponíveis antes de executar.
*
* Uso:
* ts-node scripts/test_pdf_operations.ts
*/
async function testPDFInfo() {
console.log('\n=== Testando: Informações do PDF ===');
try {
const testPdfPath = path.join(process.cwd(), 'test', 'sample.pdf');
// Verifica se o arquivo existe
try {
await fs.access(testPdfPath);
} catch {
console.log('⚠️ Arquivo de teste não encontrado. Pulando teste.');
return;
}
const info = await PDFService.getPDFInfo(testPdfPath);
console.log('✅ Informações obtidas com sucesso:');
console.log(' Páginas:', info.pages);
console.log(' Tamanho:', (info.size / 1024).toFixed(2), 'KB');
console.log(' Título:', info.title || 'N/A');
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testCompression() {
console.log('\n=== Testando: Compressão de PDF ===');
try {
const testPdfPath = path.join(process.cwd(), 'test', 'sample.pdf');
try {
await fs.access(testPdfPath);
} catch {
console.log('⚠️ Arquivo de teste não encontrado. Pulando teste.');
return;
}
const result = await PDFService.compressPDF(testPdfPath, undefined, {
quality: 95,
});
console.log('✅ Compressão concluída:');
console.log(' Tamanho original:', (result.originalSize / 1024).toFixed(2), 'KB');
console.log(' Tamanho comprimido:', (result.compressedSize / 1024).toFixed(2), 'KB');
console.log(' Redução:', result.reduction.toFixed(2), '%');
console.log(' Arquivo salvo em:', result.outputPath);
// Limpa o arquivo de teste
await fs.unlink(result.outputPath);
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testSplit() {
console.log('\n=== Testando: Divisão de PDF ===');
try {
const testPdfPath = path.join(process.cwd(), 'test', 'sample.pdf');
try {
await fs.access(testPdfPath);
} catch {
console.log('⚠️ Arquivo de teste não encontrado. Pulando teste.');
return;
}
// Primeiro, verifica quantas páginas o PDF tem
const info = await PDFService.getPDFInfo(testPdfPath);
if (info.pages < 2) {
console.log('⚠️ PDF tem menos de 2 páginas. Pulando teste de divisão.');
return;
}
const outputPaths = await PDFService.splitPDF(testPdfPath, {
splitType: 'chunks',
chunkSize: 1,
});
console.log('✅ Divisão concluída:');
console.log(' Arquivos criados:', outputPaths.length);
// Limpa os arquivos de teste
for (const outputPath of outputPaths) {
await fs.unlink(outputPath);
}
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testMerge() {
console.log('\n=== Testando: Mesclagem de PDFs ===');
try {
const testPdf1 = path.join(process.cwd(), 'test', 'sample1.pdf');
const testPdf2 = path.join(process.cwd(), 'test', 'sample2.pdf');
try {
await fs.access(testPdf1);
await fs.access(testPdf2);
} catch {
console.log('⚠️ Arquivos de teste não encontrados. Pulando teste.');
return;
}
const outputPath = await PDFService.mergePDFs({
filePaths: [testPdf1, testPdf2],
outputPath: path.join(process.cwd(), 'storage', 'pdf-output', 'test-merged.pdf'),
});
console.log('✅ Mesclagem concluída:');
console.log(' Arquivo salvo em:', outputPath);
// Limpa o arquivo de teste
await fs.unlink(outputPath);
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testImagesToPDF() {
console.log('\n=== Testando: Imagens para PDF ===');
try {
const testImage1 = path.join(process.cwd(), 'test', 'image1.png');
const testImage2 = path.join(process.cwd(), 'test', 'image2.png');
try {
await fs.access(testImage1);
} catch {
console.log('⚠️ Imagens de teste não encontradas. Pulando teste.');
return;
}
const imagePaths = [testImage1];
try {
await fs.access(testImage2);
imagePaths.push(testImage2);
} catch {
// Ignora se a segunda imagem não existe
}
const outputPath = await PDFService.imagesToPDF({
imagePaths,
outputPath: path.join(process.cwd(), 'storage', 'pdf-output', 'test-images.pdf'),
fitToPage: true,
});
console.log('✅ Conversão concluída:');
console.log(' Imagens processadas:', imagePaths.length);
console.log(' Arquivo salvo em:', outputPath);
// Limpa o arquivo de teste
await fs.unlink(outputPath);
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testPDFToImages() {
console.log('\n=== Testando: PDF para Imagens ===');
try {
const testPdfPath = path.join(process.cwd(), 'test', 'sample.pdf');
try {
await fs.access(testPdfPath);
} catch {
console.log('⚠️ Arquivo de teste não encontrado. Pulando teste.');
return;
}
const outputPaths = await PDFService.pdfToImages(testPdfPath, {
format: 'png',
quality: 95,
density: 150, // Menor DPI para teste mais rápido
pages: [1], // Apenas primeira página
});
console.log('✅ Conversão concluída:');
console.log(' Imagens criadas:', outputPaths.length);
// Limpa os arquivos de teste
for (const outputPath of outputPaths) {
await fs.unlink(outputPath);
}
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function testOCR() {
console.log('\n=== Testando: OCR ===');
console.log('⚠️ OCR pode levar vários segundos. Aguarde...');
try {
const testPdfPath = path.join(process.cwd(), 'test', 'sample-text.pdf');
try {
await fs.access(testPdfPath);
} catch {
console.log('⚠️ Arquivo de teste não encontrado. Pulando teste.');
return;
}
const result = await PDFService.performOCR(testPdfPath, {
language: 'por',
pages: [1], // Apenas primeira página
});
console.log('✅ OCR concluído:');
console.log(' Confiança:', result.confidence.toFixed(2), '%');
console.log(' Páginas processadas:', result.pages.length);
console.log(' Texto extraído (primeiros 100 caracteres):');
console.log(' ', result.text.substring(0, 100).replace(/\n/g, ' '));
} catch (error) {
console.error('❌ Erro:', error);
}
}
async function main() {
console.log('╔════════════════════════════════════════════╗');
console.log('║ Teste de Operações de PDF ║');
console.log('╚════════════════════════════════════════════╝');
console.log('\nPré-requisitos:');
console.log('- Coloque arquivos de teste na pasta "test/"');
console.log('- sample.pdf: PDF com múltiplas páginas');
console.log('- sample1.pdf e sample2.pdf: PDFs para mesclagem');
console.log('- image1.png e image2.png: Imagens para conversão');
console.log('- sample-text.pdf: PDF com texto para OCR');
// Criar diretório de teste se não existir
const testDir = path.join(process.cwd(), 'test');
try {
await fs.mkdir(testDir, { recursive: true });
} catch {
// Ignora se já existe
}
await PDFService.initialize();
await testPDFInfo();
await testCompression();
await testSplit();
await testMerge();
await testImagesToPDF();
await testPDFToImages();
await testOCR();
console.log('\n╔════════════════════════════════════════════╗');
console.log('║ Testes concluídos! ║');
console.log('╚════════════════════════════════════════════╝\n');
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error('\n❌ Erro fatal durante os testes:', error);
process.exit(1);
});
+76 -130
View File
@@ -1,87 +1,70 @@
import { LoginPage } from "./components/LoginPage";
import { Dashboard } from "./components/Dashboard";
import { MainLayout } from "./components/layouts/MainLayout";
import { AuthProvider } from "./contexts/AuthContext";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { ptBR } from "date-fns/locale";
import React from "react"; import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import { import {
createBrowserRouter, createBrowserRouter,
RouterProvider, RouterProvider,
createRoutesFromElements, createRoutesFromElements,
Route, Route,
Navigate, Navigate
} from "react-router-dom"; } from "react-router-dom";
import { LoginPage } from "./components/LoginPage";
import { ThemeProvider } from "./contexts/ThemeContext"; import { Dashboard } from "./components/Dashboard";
import { TarefasPage } from "./components/tarefas/TarefasPage"; import { MainLayout } from "./components/layouts/MainLayout";
import { DocumentosPage } from "./components/DocumentosPage"; import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/AuthContext";
import { ToastContainer } from "react-toastify"; import { ClientesPage } from "./components/clientes/ClientesPage";
import { QueryClient, QueryClientProvider } from "react-query";
import { ClienteDetalhes } from "./components/clientes/ClienteDetalhes";
import { ProcessosPage } from "./components/processos/ProcessosPage";
import { ProcessoDetalhes } from "./components/processos/ProcessoDetalhes";
import { UsuariosPage } from "./components/usuarios/UsuariosPage";
import { AdminRoute } from "./components/AdminRoute"; import { AdminRoute } from "./components/AdminRoute";
import { AtendimentoDetalhes } from "./components/atendimentos/AtendimentoDetalhes"; import { PerfilPage } from "./components/perfil/PerfilPage";
import { AtendimentosPage } from "./components/atendimentos/AtendimentosPage"; import { AtendimentosPage } from "./components/atendimentos/AtendimentosPage";
import { AtendimentoDetalhes } from "./components/atendimentos/AtendimentoDetalhes";
import "react-toastify/dist/ReactToastify.css"; import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { ptBR } from 'date-fns/locale';
import { ThemeProvider } from './contexts/ThemeContext';
import { TarefasPage } from "./components/tarefas/TarefasPage";
import { DocumentosPage } from './components/DocumentosPage';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { GerenciarTemplates } from './components/documentos/GerenciarTemplates';
import { ConfiguracaoDocumento } from './components/documentos/ConfiguracaoDocumento';
import { ProcessoAdministrativoList } from './components/processosAdministrativos/ProcessoAdministrativoList';
import { ProcessoAdminDetalhes } from './components/processosAdministrativos/ProcessoAdminDetalhes';
import { ProtectedRoute } from './components/ProtectedRoute';
import { ProvaUpload } from './components/provas/ProvaUpload';
import { ProvaStatus } from './components/provas/ProvaStatus';
import { AdminProvas } from './components/provas/AdminProvas';
import { VerificarProva } from './components/provas/VerificarProva';
import { ErrorBoundary } from './components/ErrorBoundary';
import { PublicVerificarProva } from './components/provas/PublicVerificarProva';
import { ClientProvaUpload } from './components/provas/ClientProvaUpload';
import { PublicProvaStatus } from './components/provas/PublicProvaStatus';
// Importar os componentes de cálculos // Importar os componentes de cálculos
import { CalculosList } from "./components/calculos/CalculosList"; import { CalculosList } from './components/calculos/CalculosList';
import { DetalhesCalculo } from "./components/calculos/DetalhesCalculo"; import { NovoCalculo } from './components/calculos/NovoCalculo';
import { NovoCalculo } from "./components/calculos/NovoCalculo"; import { DetalhesCalculo } from './components/calculos/DetalhesCalculo';
import { NovoPensaoAlimenticia } from "./components/calculos/NovoPensaoAlimenticia"; import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia';
import { ClienteDetalhes } from "./components/clientes/ClienteDetalhes"; import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia';
import { ClientesPage } from "./components/clientes/ClientesPage";
import { ConfiguracaoDocumento } from "./components/documentos/ConfiguracaoDocumento"; // Importar componente de manipulação de PDF
import { GerenciarTemplates } from "./components/documentos/GerenciarTemplates"; import { PDFManipulationPage } from './components/pdf/PDFManipulationPage';
import { ErrorBoundary } from "./components/ErrorBoundary";
import { PerfilPage } from "./components/perfil/PerfilPage";
import { PrivateRoute } from "./components/PrivateRoute";
import { ProcessoDetalhes } from "./components/processos/ProcessoDetalhes";
import { ProcessosPage } from "./components/processos/ProcessosPage";
import { ProcessoAdminDetalhes } from "./components/processosAdministrativos/ProcessoAdminDetalhes";
import { ProcessoAdministrativoList } from "./components/processosAdministrativos/ProcessoAdministrativoList";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { AdminProvas } from "./components/provas/AdminProvas";
import { ClientProvaUpload } from "./components/provas/ClientProvaUpload";
import { ProvaStatus } from "./components/provas/ProvaStatus";
import { ProvaUpload } from "./components/provas/ProvaUpload";
import { PublicProvaStatus } from "./components/provas/PublicProvaStatus";
import { PublicVerificarProva } from "./components/provas/PublicVerificarProva";
import { VerificarProva } from "./components/provas/VerificarProva";
import { UsuariosPage } from "./components/usuarios/UsuariosPage";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const router = createBrowserRouter( const router = createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
<> <>
<Route <Route path="/" element={<Navigate to="/dashboard" />} errorElement={<ErrorBoundary />} />
path="/"
element={<Navigate to="/dashboard" />}
errorElement={<ErrorBoundary />}
/>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
{/* Rotas públicas para validação digital */} {/* Rotas públicas para validação digital */}
<Route <Route path="/public/verificar/:hash" element={<PublicVerificarProva />} errorElement={<ErrorBoundary />} />
path="/public/verificar/:hash" <Route path="/public/provas/upload" element={<ClientProvaUpload />} errorElement={<ErrorBoundary />} />
element={<PublicVerificarProva />} <Route path="/public/provas/status/:id" element={<PublicProvaStatus />} errorElement={<ErrorBoundary />} />
errorElement={<ErrorBoundary />}
/>
<Route
path="/public/provas/upload"
element={<ClientProvaUpload />}
errorElement={<ErrorBoundary />}
/>
<Route
path="/public/provas/status/:id"
element={<PublicProvaStatus />}
errorElement={<ErrorBoundary />}
/>
<Route element={<PrivateRoute />}> <Route element={<PrivateRoute />}>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
@@ -89,18 +72,9 @@ const router = createBrowserRouter(
<Route path="documentos" element={<DocumentosPage />} /> <Route path="documentos" element={<DocumentosPage />} />
<Route path="documentos/templates" element={<GerenciarTemplates />} /> <Route path="documentos/templates" element={<GerenciarTemplates />} />
<Route path="documentos/:id" element={<DocumentosPage />} /> <Route path="documentos/:id" element={<DocumentosPage />} />
<Route <Route path="documentos/configuracoes" element={<ConfiguracaoDocumento />} />
path="documentos/configuracoes" <Route path="processos/:processoId/documentos" element={<DocumentosPage />} />
element={<ConfiguracaoDocumento />} <Route path="atendimentos/:atendimentoId/documentos" element={<DocumentosPage />} />
/>
<Route
path="processos/:processoId/documentos"
element={<DocumentosPage />}
/>
<Route
path="atendimentos/:atendimentoId/documentos"
element={<DocumentosPage />}
/>
<Route path="/processos" element={<ProcessosPage />} /> <Route path="/processos" element={<ProcessosPage />} />
<Route path="/processos/:id" element={<ProcessoDetalhes />} /> <Route path="/processos/:id" element={<ProcessoDetalhes />} />
<Route path="/clientes" element={<ClientesPage />} /> <Route path="/clientes" element={<ClientesPage />} />
@@ -109,92 +83,64 @@ const router = createBrowserRouter(
<Route path="/atendimentos" element={<AtendimentosPage />} /> <Route path="/atendimentos" element={<AtendimentosPage />} />
<Route path="/atendimentos/:id" element={<AtendimentoDetalhes />} /> <Route path="/atendimentos/:id" element={<AtendimentoDetalhes />} />
<Route path="/tarefas" element={<TarefasPage />} /> <Route path="/tarefas" element={<TarefasPage />} />
<Route <Route path="/usuarios" element={<UsuariosPage />} />
path="/processos-administrativos" <Route path="/processos-administrativos" element={<ProtectedRoute roles={['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE']}><ProcessoAdministrativoList /></ProtectedRoute>} />
element={ <Route path="/processos-administrativos/:id" element={<ProtectedRoute roles={['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE']}><ProcessoAdminDetalhes /></ProtectedRoute>} />
<ProtectedRoute
roles={["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"]}
>
<ProcessoAdministrativoList />
</ProtectedRoute>
}
/>
<Route
path="/processos-administrativos/:id"
element={
<ProtectedRoute
roles={["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"]}
>
<ProcessoAdminDetalhes />
</ProtectedRoute>
}
/>
<Route path="/provas/upload" element={<ProvaUpload />} /> <Route path="/provas/upload" element={<ProvaUpload />} />
<Route path="/provas/status/:id" element={<ProvaStatus />} /> <Route path="/provas/status/:id" element={<ProvaStatus />} />
<Route <Route path="/provas/admin" element={
path="/provas/admin" <ProtectedRoute roles={['ADVOGADO_ADMIN']}>
element={
<ProtectedRoute roles={["ADVOGADO_ADMIN"]}>
<AdminProvas /> <AdminProvas />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
<Route <Route
path="/provas/verificar/:hash" path="/provas/verificar/:hash"
element={<VerificarProva />} element={<VerificarProva />}
errorElement={<ErrorBoundary />} errorElement={<ErrorBoundary />}
/> />
{/* Rotas para Cálculos Judiciais */} {/* Rotas para Cálculos Judiciais */}
<Route <Route path="/calculos" element={
path="/calculos"
element={
<ProtectedRoute> <ProtectedRoute>
<CalculosList /> <CalculosList />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
<Route <Route path="/calculos/novo" element={
path="/calculos/novo"
element={
<ProtectedRoute> <ProtectedRoute>
<NovoCalculo /> <NovoCalculo />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
<Route <Route path="/calculos/:id" element={
path="/calculos/:id"
element={
<ProtectedRoute> <ProtectedRoute>
<DetalhesCalculo /> <DetalhesCalculo />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
{/* Rotas para Pensão Alimentícia */} {/* Rotas para Pensão Alimentícia */}
<Route <Route path="/pensao-alimenticia/novo" element={
path="/pensao-alimenticia/nova"
element={
<ProtectedRoute> <ProtectedRoute>
<NovoPensaoAlimenticia /> <NovoPensaoAlimenticia />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
<Route <Route path="/pensao-alimenticia/:id" element={
path="/pensao-alimenticia/:id"
element={
<ProtectedRoute> <ProtectedRoute>
<DetalhesCalculo /> <DetalhePensaoAlimenticia />
</ProtectedRoute> </ProtectedRoute>
} } />
/>
{/* Rota para Manipulação de PDF */}
<Route path="/pdf" element={
<ProtectedRoute>
<PDFManipulationPage />
</ProtectedRoute>
} />
</Route> </Route>
</Route> </Route>
<Route path="*" element={<Navigate to="/login" />} /> <Route path="*" element={<Navigate to="/login" />} />
</>, </>
), )
); );
export const App: React.FC = () => { export const App: React.FC = () => {
-1
View File
@@ -1,5 +1,4 @@
import { Navigate, Outlet } from "react-router-dom"; import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { PerfilUsuario } from "../types/usuario"; import { PerfilUsuario } from "../types/usuario";
-120
View File
@@ -1,120 +0,0 @@
import React, { useState, useCallback } from 'react';
import { Calendar } from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
import { format } from 'date-fns';
import { api } from '../services/api';
import { toast } from 'react-toastify';
interface Agendamento {
id: string;
data: string;
observacoes?: string;
status: string;
cliente: {
id: string;
nome: string;
};
}
interface CalendarEvent {
id: string;
title: string;
start: string;
end: string;
backgroundColor?: string;
borderColor?: string;
extendedProps: {
agendamento: Agendamento;
};
}
interface AgendamentoCalendarioProps {
onEventClick?: (event: any) => void;
onDateSelect?: (selectInfo: any) => void;
}
export const AgendamentoCalendario: React.FC<AgendamentoCalendarioProps> = ({
onEventClick,
onDateSelect
}) => {
const [eventos, setEventos] = useState<CalendarEvent[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const formatEventos = useCallback((agendamentos: Agendamento[]): CalendarEvent[] => {
return agendamentos.map(ag => ({
id: ag.id,
title: `${ag.cliente.nome} ${ag.observacoes ? `- ${ag.observacoes}` : ''}`,
start: ag.data,
end: ag.data,
backgroundColor: ag.status === 'CANCELADO' ? '#FEE2E2' : '#E0F2FE',
borderColor: ag.status === 'CANCELADO' ? '#EF4444' : '#0EA5E9',
extendedProps: {
agendamento: ag
}
}));
}, []);
const loadAgendamentos = useCallback(async ({ start, end }: { start: Date, end: Date }) => {
try {
setLoading(true);
setError(null);
const response = await api.get('/atendimentos/agendamentos', {
params: {
inicio: format(start, 'yyyy-MM-dd'),
fim: format(end, 'yyyy-MM-dd')
}
});
setEventos(formatEventos(response.data));
} catch (err: any) {
const mensagem = err.response?.data?.error || 'Erro ao carregar agendamentos';
setError(mensagem);
toast.error(mensagem);
} finally {
setLoading(false);
}
}, [formatEventos]);
return (
<div className="relative h-full">
{loading && (
<div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
</div>
)}
{error && (
<div className="absolute top-0 left-0 right-0 p-4 bg-red-100 text-red-700">
{error}
</div>
)}
<Calendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
locale={ptBrLocale}
events={eventos}
editable={false}
selectable={true}
selectMirror={true}
dayMaxEvents={3}
weekends={true}
datesSet={({ start, end }) => loadAgendamentos({ start, end })}
eventClick={({ event }) => onEventClick?.(event)}
select={(selectInfo) => onDateSelect?.(selectInfo)}
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
}}
slotMinTime="08:00:00"
slotMaxTime="18:00:00"
/>
</div>
);
};
+2 -3
View File
@@ -1,6 +1,5 @@
import React from "react"; import React from 'react';
import { DashboardLayout } from './dashboard/DashboardLayout';
import { DashboardLayout } from "./dashboard/DashboardLayout";
export const Dashboard: React.FC = () => { export const Dashboard: React.FC = () => {
return <DashboardLayout />; return <DashboardLayout />;
+57 -86
View File
@@ -1,21 +1,12 @@
import { Dialog } from "@headlessui/react"; import { useState, useEffect, useCallback } from 'react';
import { import { useParams, useNavigate, Link } from 'react-router-dom';
DocumentIcon, import { DocumentIcon, PlusIcon, TrashIcon, EyeIcon, XMarkIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline';
PlusIcon, import { DocumentoEditor } from './documentos/DocumentoEditor';
TrashIcon, import { api } from '../lib/api';
EyeIcon, import { toast } from 'react-toastify';
XMarkIcon, import { useAuth } from '../contexts/AuthContext';
DocumentArrowDownIcon, import { Dialog } from '@headlessui/react';
} from "@heroicons/react/24/outline"; import { DocumentoViewer } from './documentos/DocumentoViewer';
import { useState, useEffect, useCallback } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { toast } from "react-toastify";
import { useAuth } from "../contexts/AuthContext";
import { api } from "../lib/api";
import { DocumentoEditor } from "./documentos/DocumentoEditor";
import { DocumentoViewer } from "./documentos/DocumentoViewer";
interface Documento { interface Documento {
id: string; id: string;
@@ -48,7 +39,7 @@ export function DocumentosPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!user) { if (!user) {
navigate("/login"); navigate('/login');
return; return;
} }
@@ -56,12 +47,12 @@ export function DocumentosPage() {
setIsLoading(true); setIsLoading(true);
await Promise.all([loadDocumentos(), loadModelos()]); await Promise.all([loadDocumentos(), loadModelos()]);
} catch (error) { } catch (error) {
console.error("Erro ao carregar dados:", error); console.error('Erro ao carregar dados:', error);
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
signOut(); // Usar a função do AuthContext signOut(); // Usar a função do AuthContext
navigate("/login"); navigate('/login');
} else { } else {
toast.error("Erro ao carregar dados"); toast.error('Erro ao carregar dados');
} }
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -77,12 +68,12 @@ export function DocumentosPage() {
if (processoId) params.processoId = processoId; if (processoId) params.processoId = processoId;
if (atendimentoId) params.atendimentoId = atendimentoId; if (atendimentoId) params.atendimentoId = atendimentoId;
const response = await api.get("/documentos", { params }); const response = await api.get('/documentos', { params });
setDocumentos(response.data); setDocumentos(response.data);
}; };
const loadModelos = async () => { const loadModelos = async () => {
const response = await api.get("/documentos-modelos"); const response = await api.get('/documentos-modelos');
setModelos(response.data); setModelos(response.data);
}; };
@@ -91,40 +82,40 @@ export function DocumentosPage() {
const payload = { const payload = {
nome: data.nome.trim(), nome: data.nome.trim(),
conteudo: data.conteudo.trim(), conteudo: data.conteudo.trim(),
tipo: documentoEmEdicao?.tipo || "OUTROS", tipo: documentoEmEdicao?.tipo || 'OUTROS',
modeloId: documentoEmEdicao?.modeloId, modeloId: documentoEmEdicao?.modeloId,
processoId: data.processoId || processoId, processoId: data.processoId || processoId,
atendimentoId: data.atendimentoId || atendimentoId, atendimentoId: data.atendimentoId || atendimentoId
}; };
if (documentoEmEdicao?.id) { if (documentoEmEdicao?.id) {
await api.put(`/documentos/${documentoEmEdicao.id}`, payload); await api.put(`/documentos/${documentoEmEdicao.id}`, payload);
toast.success("Documento atualizado com sucesso"); toast.success('Documento atualizado com sucesso');
} else { } else {
await api.post("/documentos", payload); await api.post('/documentos', payload);
toast.success("Documento criado com sucesso"); toast.success('Documento criado com sucesso');
} }
await loadDocumentos(); await loadDocumentos();
setIsEditorOpen(false); setIsEditorOpen(false);
setDocumentoEmEdicao(null); setDocumentoEmEdicao(null);
} catch (error: any) { } catch (error: any) {
console.error("Erro ao salvar documento:", error); console.error('Erro ao salvar documento:', error);
toast.error(error?.response?.data?.message || "Erro ao salvar documento"); toast.error(error?.response?.data?.message || 'Erro ao salvar documento');
} }
}; };
const handleDeleteDocumento = async (id) => { const handleDeleteDocumento = async (id) => {
try { try {
await api.delete(`/documentos/${id}`); await api.delete(`/documentos/${id}`);
toast.success("Documento excluído com sucesso"); toast.success('Documento excluído com sucesso');
await loadDocumentos(); await loadDocumentos();
} catch (error) { } catch (error) {
console.error("Erro ao excluir documento:", error); console.error('Erro ao excluir documento:', error);
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
toast.error("Sessão expirada. Por favor, faça login novamente."); toast.error('Sessão expirada. Por favor, faça login novamente.');
} else { } else {
toast.error("Erro ao excluir documento"); toast.error('Erro ao excluir documento');
} }
} }
}; };
@@ -134,38 +125,34 @@ export function DocumentosPage() {
setDocumentoVisualizado(doc); // Simplificar para usar o documento diretamente setDocumentoVisualizado(doc); // Simplificar para usar o documento diretamente
setVisualizadorOpen(true); setVisualizadorOpen(true);
} catch (error) { } catch (error) {
console.error("Erro ao visualizar documento:", error); console.error('Erro ao visualizar documento:', error);
toast.error("Erro ao visualizar documento"); toast.error('Erro ao visualizar documento');
} }
}; };
const handleExportPDF = async (documento) => { const handleExportPDF = async (documento) => {
try { try {
const configRes = await api.get("/documentos/configuracoes"); const configRes = await api.get('/documentos/configuracoes');
const response = await api.post( const response = await api.post('/documentos/exportar-pdf', {
"/documentos/exportar-pdf",
{
nome: documento.nome, nome: documento.nome,
conteudo: documento.conteudo, conteudo: documento.conteudo,
escritorio: configRes.data, escritorio: configRes.data
}, }, { responseType: 'blob' });
{ responseType: "blob" },
);
// Criar URL do blob e fazer download // Criar URL do blob e fazer download
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a"); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute("download", `${documento.nome}.pdf`); link.setAttribute('download', `${documento.nome}.pdf`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
link.parentNode.removeChild(link); link.parentNode.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
toast.success("PDF gerado com sucesso!"); toast.success('PDF gerado com sucesso!');
} catch (error) { } catch (error) {
console.error("Erro ao gerar PDF:", error); console.error('Erro ao gerar PDF:', error);
toast.error("Erro ao gerar PDF"); toast.error('Erro ao gerar PDF');
} }
}; };
@@ -175,10 +162,10 @@ export function DocumentosPage() {
id: documento.id, id: documento.id,
nome: documento.nome, nome: documento.nome,
conteudo: documento.conteudo, conteudo: documento.conteudo,
tipo: documento.tipo || "OUTROS", tipo: documento.tipo || 'OUTROS',
modeloId: documento.modeloId, modeloId: documento.modeloId,
processoId: documento.processoId || processoId, processoId: documento.processoId || processoId,
atendimentoId: documento.atendimentoId || atendimentoId, atendimentoId: documento.atendimentoId || atendimentoId
}); });
setVisualizadorOpen(false); setVisualizadorOpen(false);
setIsEditorOpen(true); setIsEditorOpen(true);
@@ -189,9 +176,9 @@ export function DocumentosPage() {
nome: modelo.nome, nome: modelo.nome,
conteudo: modelo.conteudo, conteudo: modelo.conteudo,
modeloId: modelo.id, modeloId: modelo.id,
tipo: "OUTROS", tipo: 'OUTROS',
processoId: processoId, processoId: processoId,
atendimentoId: atendimentoId, atendimentoId: atendimentoId
}); });
setIsEditorOpen(true); setIsEditorOpen(true);
}; };
@@ -212,9 +199,9 @@ export function DocumentosPage() {
<PlusIcon className="w-5 h-5 mr-2" /> <PlusIcon className="w-5 h-5 mr-2" />
Novo Documento Novo Documento
</button> </button>
{user?.perfil === "ADVOGADO_ADMIN" && ( {user?.perfil === 'ADVOGADO_ADMIN' && (
<button <button
onClick={() => navigate("/documentos/templates")} onClick={() => navigate('/documentos/templates')}
className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors"
> >
<DocumentIcon className="w-5 h-5 mr-2" /> <DocumentIcon className="w-5 h-5 mr-2" />
@@ -230,8 +217,8 @@ export function DocumentosPage() {
onClick={() => setTabValue(0)} onClick={() => setTabValue(0)}
className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${ className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 0 tabValue === 0
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Documentos Documentos
@@ -240,8 +227,8 @@ export function DocumentosPage() {
onClick={() => setTabValue(1)} onClick={() => setTabValue(1)}
className={`ml-8 px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${ className={`ml-8 px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 1 tabValue === 1
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Modelos Modelos
@@ -252,24 +239,18 @@ export function DocumentosPage() {
{tabValue === 0 && ( {tabValue === 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{documentos.map((doc) => ( {documentos.map((doc) => (
<div <div key={doc.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
key={doc.id} <div className="p-4 min-h-[180px] flex flex-col justify-between"> {/* Ajustado min-height e layout */}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4"
>
<div className="p-4 min-h-[180px] flex flex-col justify-between">
{" "}
{/* Ajustado min-height e layout */}
<div> <div>
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2 flex-shrink-0" /> <DocumentIcon className="w-6 h-6 text-gray-500 mr-2 flex-shrink-0" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100 line-clamp-2"> <h3 className="font-semibold text-gray-900 dark:text-gray-100 line-clamp-2">{doc.nome}</h3>
{doc.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Criado em: {new Date(doc.criadoEm).toLocaleDateString()} Criado em: {new Date(doc.criadoEm).toLocaleDateString()}
</p> </p>
</div> </div>
{/* Botões em flex-wrap para melhor responsividade */} {/* Botões em flex-wrap para melhor responsividade */}
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
<button <button
@@ -303,20 +284,13 @@ export function DocumentosPage() {
{tabValue === 1 && ( {tabValue === 1 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modelos.map((modelo) => ( {modelos.map((modelo) => (
<div <div key={modelo.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
key={modelo.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<div className="p-4"> <div className="p-4">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" /> <DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100"> <h3 className="font-semibold text-gray-900 dark:text-gray-100">{modelo.nome}</h3>
{modelo.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{modelo.descricao}</p>
{modelo.descricao}
</p>
<button <button
onClick={() => handleUseModelo(modelo)} onClick={() => handleUseModelo(modelo)}
className="w-full inline-flex items-center justify-center px-4 py-2 text-sm border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20" className="w-full inline-flex items-center justify-center px-4 py-2 text-sm border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20"
@@ -353,17 +327,13 @@ export function DocumentosPage() {
<div className="p-6 max-h-[70vh] overflow-y-auto"> <div className="p-6 max-h-[70vh] overflow-y-auto">
<div <div
className="prose max-w-none" className="prose max-w-none"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{ __html: documentoVisualizado?.conteudo }}
__html: documentoVisualizado?.conteudo,
}}
/> />
</div> </div>
<div className="flex items-center justify-between p-4 border-t bg-gray-50"> <div className="flex items-center justify-between p-4 border-t bg-gray-50">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
Criado em:{" "} Criado em: {documentoVisualizado && new Date(documentoVisualizado.criadoEm).toLocaleDateString()}
{documentoVisualizado &&
new Date(documentoVisualizado.criadoEm).toLocaleDateString()}
</div> </div>
<button <button
@@ -372,6 +342,7 @@ export function DocumentosPage() {
> >
Editar Editar
</button> </button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</div> </div>
+7 -17
View File
@@ -1,7 +1,6 @@
import React from "react"; import React from 'react';
import { isRouteErrorResponse, useRouteError } from "react-router-dom"; import { Card } from './ui/Card';
import { isRouteErrorResponse, useRouteError } from 'react-router-dom';
import { Card } from "./ui/Card";
export function ErrorBoundary() { export function ErrorBoundary() {
const error = useRouteError(); const error = useRouteError();
@@ -19,24 +18,15 @@ export function ErrorBoundary() {
<div className="mt-4 p-4 bg-red-50 rounded-lg text-left"> <div className="mt-4 p-4 bg-red-50 rounded-lg text-left">
{isRouteErrorResponse(error) ? ( {isRouteErrorResponse(error) ? (
<div> <div>
<p> <p><strong>Status:</strong> {error.status}</p>
<strong>Status:</strong> {error.status} <p><strong>Mensagem:</strong> {error.data.message || error.statusText}</p>
</p>
<p>
<strong>Mensagem:</strong>{" "}
{error.data.message || error.statusText}
</p>
</div> </div>
) : error instanceof Error ? ( ) : error instanceof Error ? (
<div> <div>
<p> <p><strong>Erro:</strong> {error.message}</p>
<strong>Erro:</strong> {error.message}
</p>
{error.stack && ( {error.stack && (
<details className="mt-2"> <details className="mt-2">
<summary className="cursor-pointer text-sm"> <summary className="cursor-pointer text-sm">Ver detalhes técnicos</summary>
Ver detalhes técnicos
</summary>
<pre className="mt-2 whitespace-pre-wrap text-xs"> <pre className="mt-2 whitespace-pre-wrap text-xs">
{error.stack} {error.stack}
</pre> </pre>
-1
View File
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
export const LoginPage: React.FC = () => { export const LoginPage: React.FC = () => {
-1
View File
@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom"; import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
export const PrivateRoute: React.FC = () => { export const PrivateRoute: React.FC = () => {
+11 -12
View File
@@ -1,6 +1,5 @@
import { Navigate, useLocation } from "react-router-dom"; import { useAuth } from '../contexts/AuthContext';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from "../contexts/AuthContext";
interface ProtectedRouteProps { interface ProtectedRouteProps {
children: React.ReactNode; children: React.ReactNode;
@@ -11,27 +10,27 @@ export function ProtectedRoute({ children, roles }: ProtectedRouteProps) {
const { user } = useAuth(); const { user } = useAuth();
const location = useLocation(); const location = useLocation();
console.log("ProtectedRoute - Current Path:", location.pathname); console.log('ProtectedRoute - Current Path:', location.pathname);
console.log("ProtectedRoute - Required Roles:", roles); console.log('ProtectedRoute - Required Roles:', roles);
console.log("ProtectedRoute - User:", user); console.log('ProtectedRoute - User:', user);
if (!user) { if (!user) {
console.log("ProtectedRoute - No user, redirecting to login"); console.log('ProtectedRoute - No user, redirecting to login');
return <Navigate to="/login" />; return <Navigate to="/login" />;
} }
if (!roles || roles.length === 0) { if (!roles || roles.length === 0) {
console.log("ProtectedRoute - No roles required, allowing access"); console.log('ProtectedRoute - No roles required, allowing access');
return <>{children}</>; return <>{children}</>;
} }
if (!roles.includes(user.perfil)) { if (!roles.includes(user.perfil)) {
console.log("ProtectedRoute - Access denied"); console.log('ProtectedRoute - Access denied');
console.log("Required roles:", roles); console.log('Required roles:', roles);
console.log("User role:", user.perfil); console.log('User role:', user.perfil);
return <Navigate to="/dashboard" replace state={{ from: location }} />; return <Navigate to="/dashboard" replace state={{ from: location }} />;
} }
console.log("ProtectedRoute - Access granted"); console.log('ProtectedRoute - Access granted');
return <>{children}</>; return <>{children}</>;
} }
+10 -10
View File
@@ -1,3 +1,4 @@
import { useLocation } from 'react-router-dom';
import { import {
HomeIcon, HomeIcon,
ScaleIcon, ScaleIcon,
@@ -5,21 +6,20 @@ import {
UserIcon, UserIcon,
ClipboardDocumentCheckIcon, ClipboardDocumentCheckIcon,
DocumentTextIcon, DocumentTextIcon,
} from "@heroicons/react/24/outline"; } from '@heroicons/react/24/outline';
import { useLocation } from "react-router-dom";
export function useNavigation() { export function useNavigation() {
const { pathname } = useLocation(); const { pathname } = useLocation();
return [ return [
{ name: "Dashboard", href: "/dashboard", icon: HomeIcon }, { name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
{ name: "Processos", href: "/processos", icon: ScaleIcon }, { name: 'Processos', href: '/processos', icon: ScaleIcon },
{ name: "Clientes", href: "/clientes", icon: UserGroupIcon }, { name: 'Clientes', href: '/clientes', icon: UserGroupIcon },
{ name: "Atendimentos", href: "/atendimentos", icon: UserIcon }, { name: 'Atendimentos', href: '/atendimentos', icon: UserIcon },
{ name: "Tarefas", href: "/tarefas", icon: ClipboardDocumentCheckIcon }, { name: 'Tarefas', href: '/tarefas', icon: ClipboardDocumentCheckIcon },
{ name: "Documentos", href: "/documentos", icon: DocumentTextIcon }, { name: 'Documentos', href: '/documentos', icon: DocumentTextIcon },
].map((item) => ({ ].map(item => ({
...item, ...item,
current: pathname === item.href, current: pathname === item.href
})); }));
} }
+21 -37
View File
@@ -1,7 +1,6 @@
import { DateTimePicker } from "@mui/x-date-pickers"; import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect } from "react"; import { DateTimePicker } from '@mui/x-date-pickers';
import api from '../../services/api';
import api from "../../services/api";
// Implementar debounce // Implementar debounce
function debounce(fn: Function, ms = 300) { function debounce(fn: Function, ms = 300) {
@@ -29,24 +28,18 @@ interface AgendamentoFormProps {
}; };
} }
export function AgendamentoForm({ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClienteId, agendamento }: AgendamentoFormProps) {
open,
onClose,
onSave,
clienteId: defaultClienteId,
agendamento,
}: AgendamentoFormProps) {
const [data, setData] = useState<Date | null>(null); const [data, setData] = useState<Date | null>(null);
const [observacoes, setObservacoes] = useState(""); const [observacoes, setObservacoes] = useState('');
const [error, setError] = useState(""); const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [tipo, setTipo] = useState(""); const [tipo, setTipo] = useState('');
// Adicionar estados para busca de cliente // Adicionar estados para busca de cliente
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]); const [searchResults, setSearchResults] = useState<any[]>([]);
const [selectedClient, setSelectedClient] = useState<any>(null); const [selectedClient, setSelectedClient] = useState<any>(null);
const [clienteId, setClienteId] = useState(defaultClienteId || ""); const [clienteId, setClienteId] = useState(defaultClienteId || '');
// Preencher form quando editar // Preencher form quando editar
useEffect(() => { useEffect(() => {
@@ -54,8 +47,8 @@ export function AgendamentoForm({
setData(new Date(agendamento.data)); setData(new Date(agendamento.data));
setClienteId(agendamento.cliente.id); setClienteId(agendamento.cliente.id);
setSearchTerm(agendamento.cliente.nome); setSearchTerm(agendamento.cliente.nome);
setTipo(agendamento.tipo || ""); setTipo(agendamento.tipo || '');
setObservacoes(agendamento.observacoes || ""); setObservacoes(agendamento.observacoes || '');
} }
}, [agendamento]); }, [agendamento]);
@@ -67,16 +60,16 @@ export function AgendamentoForm({
const response = await api.get(`/clientes/search?q=${term}`); const response = await api.get(`/clientes/search?q=${term}`);
setSearchResults(response.data); setSearchResults(response.data);
} catch (error) { } catch (error) {
console.error("Erro ao buscar clientes:", error); console.error('Erro ao buscar clientes:', error);
} }
}, 300), }, 300),
[], []
); );
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!data || !clienteId) { if (!data || !clienteId) {
setError("Data e cliente são obrigatórios"); setError('Data e cliente são obrigatórios');
return; return;
} }
@@ -84,7 +77,7 @@ export function AgendamentoForm({
setIsSaving(true); setIsSaving(true);
if (agendamento) { if (agendamento) {
// Atualizar agendamento existente // Atualizar agendamento existente
await api.put(`/atendimentos/agendamentos/${agendamento.id}`, { await api.put(`/agendamentos/${agendamento.id}`, {
clienteId, clienteId,
data: data.toISOString(), data: data.toISOString(),
observacoes, observacoes,
@@ -92,7 +85,7 @@ export function AgendamentoForm({
}); });
} else { } else {
// Criar novo agendamento // Criar novo agendamento
await api.post("/atendimentos/agendamentos", { await api.post('/agendamentos', {
clienteId, clienteId,
data: data.toISOString(), data: data.toISOString(),
observacoes, observacoes,
@@ -102,21 +95,14 @@ export function AgendamentoForm({
onSave(); onSave();
onClose(); onClose();
} catch (error) { } catch (error) {
console.error("Erro ao salvar agendamento:", error); setError(agendamento ? 'Erro ao atualizar agendamento' : 'Erro ao criar agendamento');
setError(
agendamento
? "Erro ao atualizar agendamento"
: "Erro ao criar agendamento",
);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
return ( return (
<div <div className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? 'flex' : 'hidden'} z-50`}>
className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? "flex" : "hidden"} z-50`}
>
<div className="relative bg-white rounded-lg max-w-md w-full m-auto p-6"> <div className="relative bg-white rounded-lg max-w-md w-full m-auto p-6">
<h2 className="text-xl font-bold mb-6">Agendar Atendimento</h2> <h2 className="text-xl font-bold mb-6">Agendar Atendimento</h2>
@@ -139,7 +125,7 @@ export function AgendamentoForm({
{searchResults.length > 0 && ( {searchResults.length > 0 && (
<div className="absolute z-20 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg"> <div className="absolute z-20 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg">
{searchResults.map((client) => ( {searchResults.map(client => (
<div <div
key={client.id} key={client.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer" className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@@ -151,9 +137,7 @@ export function AgendamentoForm({
}} }}
> >
<div className="font-medium">{client.nome}</div> <div className="font-medium">{client.nome}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">{client.cpfCnpj}</div>
{client.cpfCnpj}
</div>
</div> </div>
))} ))}
</div> </div>
@@ -214,7 +198,7 @@ export function AgendamentoForm({
disabled={isSaving} disabled={isSaving}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50"
> >
{isSaving ? "Salvando..." : "Agendar"} {isSaving ? 'Salvando...' : 'Agendar'}
</button> </button>
</div> </div>
</form> </form>
@@ -1,23 +1,19 @@
import { format } from "date-fns"; import { useState } from 'react';
import { ptBR } from "date-fns/locale"; import { useQuery } from 'react-query';
import { useState } from "react"; import { useParams, Link } from 'react-router-dom';
import { useQuery } from "react-query"; import api from '../../services/api';
import { useParams, Link } from "react-router-dom"; import { format, parseISO } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import api from "../../services/api";
// Função auxiliar para formatar data com validação // Função auxiliar para formatar data com validação
const formatDate = ( const formatDate = (date: string | Date | null | undefined, formatStr: string): string => {
date: string | Date | null | undefined, if (!date) return 'Data não disponível';
formatStr: string,
): string => {
if (!date) return "Data não disponível";
try { try {
const dateObj = typeof date === "string" ? parseISO(date) : date; const dateObj = typeof date === 'string' ? parseISO(date) : date;
return format(dateObj, formatStr, { locale: ptBR }); return format(dateObj, formatStr, { locale: ptBR });
} catch (error) { } catch (error) {
console.error("Erro ao formatar data:", error); console.error('Erro ao formatar data:', error);
return "Data inválida"; return 'Data inválida';
} }
}; };
@@ -25,34 +21,31 @@ export function AtendimentoDetalhes() {
const { id } = useParams(); const { id } = useParams();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [cancelModalOpen, setCancelModalOpen] = useState(false); const [cancelModalOpen, setCancelModalOpen] = useState(false);
const [justificativa, setJustificativa] = useState(""); const [justificativa, setJustificativa] = useState('');
const [isCancelling, setIsCancelling] = useState(false); const [isCancelling, setIsCancelling] = useState(false);
const { data: atendimento, refetch } = useQuery( const { data: atendimento, refetch } = useQuery(['atendimento', id], async () => {
["atendimento", id],
async () => {
const response = await api.get(`/atendimentos/${id}`); const response = await api.get(`/atendimentos/${id}`);
return response.data; return response.data;
}, });
);
const handleCancelar = async () => { const handleCancelar = async () => {
if (!justificativa.trim()) { if (!justificativa.trim()) {
alert("A justificativa é obrigatória"); alert('A justificativa é obrigatória');
return; return;
} }
try { try {
setIsCancelling(true); setIsCancelling(true);
await api.patch(`/atendimentos/${id}/status`, { await api.patch(`/atendimentos/${id}/status`, {
status: "CANCELADO", status: 'CANCELADO',
justificativa, justificativa
}); });
await refetch(); await refetch();
setCancelModalOpen(false); setCancelModalOpen(false);
} catch (error) { } catch (error) {
console.error("Erro ao cancelar atendimento:", error); console.error('Erro ao cancelar atendimento:', error);
alert("Erro ao cancelar atendimento"); alert('Erro ao cancelar atendimento');
} finally { } finally {
setIsCancelling(false); setIsCancelling(false);
} }
@@ -73,20 +66,12 @@ export function AtendimentoDetalhes() {
<div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4"> <div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-2xl font-bold text-white"> <h1 className="text-2xl font-bold text-white">{atendimento.titulo}</h1>
{atendimento.titulo} <p className="text-blue-100">Protocolo: {atendimento.protocolo}</p>
</h1>
<p className="text-blue-100">
Protocolo: {atendimento.protocolo}
</p>
</div> </div>
<span <span className={`px-3 py-1 rounded-full text-sm font-medium ${
className={`px-3 py-1 rounded-full text-sm font-medium ${ atendimento.tipo === 'INICIAL' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'
atendimento.tipo === "INICIAL" }`}>
? "bg-green-100 text-green-800"
: "bg-blue-100 text-blue-800"
}`}
>
{atendimento.tipo} {atendimento.tipo}
</span> </span>
</div> </div>
@@ -97,23 +82,17 @@ export function AtendimentoDetalhes() {
<div className="grid grid-cols-2 gap-6 mb-6"> <div className="grid grid-cols-2 gap-6 mb-6">
<div> <div>
<h3 className="text-sm font-medium text-gray-500">Cliente</h3> <h3 className="text-sm font-medium text-gray-500">Cliente</h3>
<p className="mt-1 text-sm text-gray-900"> <p className="mt-1 text-sm text-gray-900">{atendimento.cliente.nome}</p>
{atendimento.cliente.nome}
</p>
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-gray-500"> <h3 className="text-sm font-medium text-gray-500">Data do Atendimento</h3>
Data do Atendimento
</h3>
<p className="mt-1 text-sm text-gray-900"> <p className="mt-1 text-sm text-gray-900">
{formatDate(atendimento.dataAtendimento, "PPpp")} {formatDate(atendimento.dataAtendimento, "PPpp")}
</p> </p>
</div> </div>
{atendimento.processo && ( {atendimento.processo && (
<div> <div>
<h3 className="text-sm font-medium text-gray-500"> <h3 className="text-sm font-medium text-gray-500">Processo Vinculado</h3>
Processo Vinculado
</h3>
<Link <Link
to={`/processos/${atendimento.processo.id}`} to={`/processos/${atendimento.processo.id}`}
className="mt-1 text-sm text-blue-600 hover:text-blue-800" className="mt-1 text-sm text-blue-600 hover:text-blue-800"
@@ -124,9 +103,7 @@ export function AtendimentoDetalhes() {
)} )}
<div> <div>
<h3 className="text-sm font-medium text-gray-500">Responsável</h3> <h3 className="text-sm font-medium text-gray-500">Responsável</h3>
<p className="mt-1 text-sm text-gray-900"> <p className="mt-1 text-sm text-gray-900">{atendimento.usuario.nome}</p>
{atendimento.usuario.nome}
</p>
</div> </div>
</div> </div>
@@ -145,22 +122,9 @@ export function AtendimentoDetalhes() {
<h3 className="text-lg font-medium text-gray-900 mb-4">Anexos</h3> <h3 className="text-lg font-medium text-gray-900 mb-4">Anexos</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{atendimento.anexos.map((anexo: any) => ( {atendimento.anexos.map((anexo: any) => (
<div <div key={anexo.id} className="flex items-center p-4 bg-gray-50 rounded-lg">
key={anexo.id} <svg className="w-6 h-6 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
className="flex items-center p-4 bg-gray-50 rounded-lg" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
>
<svg
className="w-6 h-6 text-gray-400 mr-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
/>
</svg> </svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"> <p className="text-sm font-medium text-gray-900 truncate">
@@ -173,7 +137,7 @@ export function AtendimentoDetalhes() {
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
window.open(anexo.url, "_blank", "noopener,noreferrer"); window.open(anexo.url, '_blank', 'noopener,noreferrer');
}} }}
className="ml-4 text-blue-600 hover:text-blue-800" className="ml-4 text-blue-600 hover:text-blue-800"
> >
@@ -186,7 +150,7 @@ export function AtendimentoDetalhes() {
)} )}
{/* Adicionar botão de cancelar próximo ao cabeçalho */} {/* Adicionar botão de cancelar próximo ao cabeçalho */}
{atendimento.status !== "CANCELADO" && ( {atendimento.status !== 'CANCELADO' && (
<button <button
onClick={() => setCancelModalOpen(true)} onClick={() => setCancelModalOpen(true)}
className="bg-red-100 text-red-600 px-4 py-2 rounded-lg hover:bg-red-200" className="bg-red-100 text-red-600 px-4 py-2 rounded-lg hover:bg-red-200"
@@ -199,12 +163,9 @@ export function AtendimentoDetalhes() {
{cancelModalOpen && ( {cancelModalOpen && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4"> <div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6"> <div className="bg-white rounded-lg max-w-md w-full p-6">
<h3 className="text-lg font-medium mb-4"> <h3 className="text-lg font-medium mb-4">Cancelar Atendimento</h3>
Cancelar Atendimento
</h3>
<p className="text-sm text-gray-500 mb-4"> <p className="text-sm text-gray-500 mb-4">
Esta ação não pode ser desfeita. O atendimento será marcado Esta ação não pode ser desfeita. O atendimento será marcado como cancelado, mas manterá seu protocolo e histórico.
como cancelado, mas manterá seu protocolo e histórico.
</p> </p>
<textarea <textarea
value={justificativa} value={justificativa}
@@ -226,7 +187,7 @@ export function AtendimentoDetalhes() {
disabled={isCancelling || !justificativa.trim()} disabled={isCancelling || !justificativa.trim()}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50" className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
> >
{isCancelling ? "Cancelando..." : "Confirmar Cancelamento"} {isCancelling ? 'Cancelando...' : 'Confirmar Cancelamento'}
</button> </button>
</div> </div>
</div> </div>
@@ -234,14 +195,10 @@ export function AtendimentoDetalhes() {
)} )}
{/* Exibir informação de cancelamento se aplicável */} {/* Exibir informação de cancelamento se aplicável */}
{atendimento.status === "CANCELADO" && ( {atendimento.status === 'CANCELADO' && (
<div className="mt-4 p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg"> <div className="mt-4 p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
<h4 className="text-red-800 font-medium"> <h4 className="text-red-800 font-medium">Atendimento Cancelado</h4>
Atendimento Cancelado <p className="text-red-600 mt-1">{atendimento.justificativaCancelamento}</p>
</h4>
<p className="text-red-600 mt-1">
{atendimento.justificativaCancelamento}
</p>
<p className="text-sm text-red-500 mt-1"> <p className="text-sm text-red-500 mt-1">
Cancelado em: {formatDate(atendimento.dataAtualizacao, "PPpp")} Cancelado em: {formatDate(atendimento.dataAtualizacao, "PPpp")}
</p> </p>
+75 -131
View File
@@ -1,26 +1,13 @@
import { XMarkIcon, PaperClipIcon } from "@heroicons/react/24/outline"; // Alterado para v2 import { useState, useEffect, useCallback } from 'react';
import { import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, FormControl, InputLabel, Select, MenuItem, Alert } from '@mui/material';
Dialog, import { useEditor, EditorContent } from '@tiptap/react';
DialogTitle, import StarterKit from '@tiptap/starter-kit';
DialogContent, import Link from '@tiptap/extension-link';
DialogActions, import { TipoAtendimento } from '@prisma/client';
Button, import api from '../../services/api'; // Adicionar esta importação
TextField, import { XMarkIcon, PaperClipIcon } from '@heroicons/react/24/outline'; // Alterado para v2
FormControl, import { useDropzone } from 'react-dropzone';
InputLabel, import { uploadFile } from '../../services/upload';
Select,
MenuItem,
Alert,
} from "@mui/material";
import { TipoAtendimento } from "@prisma/client";
import Link from "@tiptap/extension-link";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useState, useEffect, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import api from "../../services/api"; // Adicionar esta importação
import { uploadFile } from "../../services/upload";
// Implementar debounce manualmente ao invés de usar lodash // Implementar debounce manualmente ao invés de usar lodash
function debounce(fn: Function, ms = 300) { function debounce(fn: Function, ms = 300) {
@@ -53,26 +40,19 @@ interface AnexoTemp {
erro?: string; erro?: string;
} }
export function AtendimentoForm({ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId, initialData }: AtendimentoFormProps) {
open, const [searchTerm, setSearchTerm] = useState('');
onClose,
onSave,
clienteId,
processoId,
initialData,
}: AtendimentoFormProps) {
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState<any[]>([]); const [searchResults, setSearchResults] = useState<any[]>([]);
const [selectedClient, setSelectedClient] = useState<any>(null); const [selectedClient, setSelectedClient] = useState<any>(null);
const [atendimentoInicial, setAtendimentoInicial] = useState<any>(null); const [atendimentoInicial, setAtendimentoInicial] = useState<any>(null);
const [atendimentosIniciais, setAtendimentosIniciais] = useState<any[]>([]); const [atendimentosIniciais, setAtendimentosIniciais] = useState<any[]>([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
titulo: "", titulo: '',
tipo: TipoAtendimento.INICIAL, tipo: TipoAtendimento.INICIAL,
conteudo: "", conteudo: '',
clienteId: clienteId || "", clienteId: clienteId || '',
processoId: processoId || "", processoId: processoId || '',
atendimentoInicialId: "", atendimentoInicialId: '',
}); });
const [anexos, setAnexos] = useState<AnexoTemp[]>([]); const [anexos, setAnexos] = useState<AnexoTemp[]>([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@@ -81,42 +61,41 @@ export function AtendimentoForm({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setFormData({ setFormData({
titulo: "", titulo: '',
tipo: TipoAtendimento.INICIAL, tipo: TipoAtendimento.INICIAL,
conteudo: "", conteudo: '',
clienteId: clienteId || "", clienteId: clienteId || '',
processoId: processoId || "", processoId: processoId || '',
atendimentoInicialId: "", atendimentoInicialId: '',
}); });
} }
}, [open, clienteId, processoId]); }, [open, clienteId, processoId]);
useEffect(() => { useEffect(() => {
if (open && initialData) { if (open && initialData) {
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
titulo: initialData.titulo || prev.titulo, titulo: initialData.titulo || prev.titulo,
clienteId: clienteId || prev.clienteId, clienteId: clienteId || prev.clienteId,
processoId: processoId || prev.processoId, processoId: processoId || prev.processoId,
tipo: initialData.tipo || prev.tipo, tipo: initialData.tipo || prev.tipo,
atendimentoInicialId: atendimentoInicialId: initialData.atendimentoInicialId || prev.atendimentoInicialId
initialData.atendimentoInicialId || prev.atendimentoInicialId,
})); }));
} }
}, [open, initialData, clienteId, processoId]); }, [open, initialData, clienteId, processoId]);
const [clientes, setClientes] = useState<any[]>([]); const [clientes, setClientes] = useState<any[]>([]);
const [error, setError] = useState(""); const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false); // Adicionar estado para prevenir múltiplos submits const [isSaving, setIsSaving] = useState(false); // Adicionar estado para prevenir múltiplos submits
// Buscar lista de clientes // Buscar lista de clientes
useEffect(() => { useEffect(() => {
const fetchClientes = async () => { const fetchClientes = async () => {
try { try {
const response = await api.get("/clientes"); const response = await api.get('/clientes');
setClientes(response.data); setClientes(response.data);
} catch (err) { } catch (err) {
console.error("Erro ao carregar clientes:", err); console.error('Erro ao carregar clientes:', err);
} }
}; };
fetchClientes(); fetchClientes();
@@ -130,21 +109,18 @@ export function AtendimentoForm({
const response = await api.get(`/clientes/search?q=${term}`); const response = await api.get(`/clientes/search?q=${term}`);
setSearchResults(response.data); setSearchResults(response.data);
} catch (error) { } catch (error) {
console.error("Erro ao buscar clientes:", error); console.error('Erro ao buscar clientes:', error);
} }
}, 300), }, 300),
[], []
); );
// Carregar atendimentos iniciais quando selecionar cliente // Carregar atendimentos iniciais quando selecionar cliente
useEffect(() => { useEffect(() => {
if (formData.clienteId && formData.tipo !== TipoAtendimento.INICIAL) { if (formData.clienteId && formData.tipo !== TipoAtendimento.INICIAL) {
api api.get(`/atendimentos/iniciais/${formData.clienteId}`)
.get(`/atendimentos/iniciais/${formData.clienteId}`) .then(response => setAtendimentosIniciais(response.data))
.then((response) => setAtendimentosIniciais(response.data)) .catch(error => console.error('Erro ao carregar atendimentos:', error));
.catch((error) =>
console.error("Erro ao carregar atendimentos:", error),
);
} }
}, [formData.clienteId, formData.tipo]); }, [formData.clienteId, formData.tipo]);
@@ -155,28 +131,28 @@ export function AtendimentoForm({
openOnClick: false, openOnClick: false,
}), }),
], ],
content: "", content: '',
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
setFormData((prev) => ({ ...prev, conteudo: editor.getHTML() })); setFormData(prev => ({ ...prev, conteudo: editor.getHTML() }));
}, },
}); });
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
const novosAnexos = acceptedFiles.map((file) => ({ const novosAnexos = acceptedFiles.map(file => ({
file, file,
nome: file.name, nome: file.name,
progresso: 0, progresso: 0
})); }));
setAnexos((prev) => [...prev, ...novosAnexos]); setAnexos(prev => [...prev, ...novosAnexos]);
}, []); }, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
maxSize: 10485760, // 10MB maxSize: 10485760 // 10MB
}); });
const handleRemoveAnexo = (index: number) => { const handleRemoveAnexo = (index: number) => {
setAnexos((prev) => prev.filter((_, i) => i !== index)); setAnexos(prev => prev.filter((_, i) => i !== index));
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@@ -185,10 +161,10 @@ export function AtendimentoForm({
try { try {
setIsSaving(true); setIsSaving(true);
setError(""); setError('');
if (!formData.clienteId) { if (!formData.clienteId) {
setError("Selecione um cliente"); setError('Selecione um cliente');
setIsSaving(false); setIsSaving(false);
return; return;
} }
@@ -199,15 +175,15 @@ export function AtendimentoForm({
const response = await api.get(`/clientes/${formData.clienteId}`); const response = await api.get(`/clientes/${formData.clienteId}`);
setSelectedClient(response.data); setSelectedClient(response.data);
} catch (error) { } catch (error) {
console.error("Erro ao buscar cliente:", error); console.error('Erro ao buscar cliente:', error);
setError("Erro ao buscar dados do cliente"); setError('Erro ao buscar dados do cliente');
setIsSaving(false); setIsSaving(false);
return; return;
} }
} }
// Criar atendimento primeiro para ter o protocolo // Criar atendimento primeiro para ter o protocolo
const response = await api.post("/atendimentos", { const response = await api.post('/atendimentos', {
...formData, ...formData,
agendamentoId: initialData?.agendamentoId, agendamentoId: initialData?.agendamentoId,
}); });
@@ -219,15 +195,15 @@ export function AtendimentoForm({
if (anexos.length > 0) { if (anexos.length > 0) {
const anexosPromises = anexos.map(async (anexo) => { const anexosPromises = anexos.map(async (anexo) => {
try { try {
const clienteNome = selectedClient?.nome || "cliente"; const clienteNome = selectedClient?.nome || 'cliente';
return await uploadFile( return await uploadFile(
anexo.file, anexo.file,
clienteNome, clienteNome,
"atendimento", 'atendimento',
protocolo, protocolo
); );
} catch (error) { } catch (error) {
console.error("Erro no upload:", error); console.error('Erro no upload:', error);
return null; return null;
} }
}); });
@@ -237,42 +213,38 @@ export function AtendimentoForm({
if (anexosSalvos.length > 0) { if (anexosSalvos.length > 0) {
await api.patch(`/atendimentos/${atendimentoId}`, { await api.patch(`/atendimentos/${atendimentoId}`, {
anexos: anexosSalvos, anexos: anexosSalvos
}); });
} }
} }
// Limpar form e fechar modal // Limpar form e fechar modal
setFormData({ setFormData({
titulo: "", titulo: '',
tipo: TipoAtendimento.INICIAL, tipo: TipoAtendimento.INICIAL,
conteudo: "", conteudo: '',
clienteId: "", clienteId: '',
processoId: "", processoId: '',
atendimentoInicialId: "", atendimentoInicialId: '',
}); });
await onSave(response.data); await onSave(response.data);
onClose(); onClose();
} catch (err) { } catch (err) {
console.error("Erro ao salvar atendimento:", err); console.error('Erro ao salvar atendimento:', err);
setError(err.response?.data?.error || "Erro ao criar atendimento"); setError(err.response?.data?.error || 'Erro ao criar atendimento');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
return ( return (
<div <div className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? 'flex' : 'hidden'} z-50`}>
className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? "flex" : "hidden"} z-50`}
>
<div className="relative bg-white rounded-lg max-w-4xl w-full m-auto p-6 overflow-y-auto max-h-[90vh]"> <div className="relative bg-white rounded-lg max-w-4xl w-full m-auto p-6 overflow-y-auto max-h-[90vh]">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Novo Atendimento</h2> <h2 className="text-2xl font-bold text-gray-900">Novo Atendimento</h2>
<button <button onClick={onClose} className="text-gray-400 hover:text-gray-500">
onClick={onClose}
className="text-gray-400 hover:text-gray-500"
>
<XMarkIcon className="h-6 w-6" /> {/* Alterado para XMarkIcon */} <XMarkIcon className="h-6 w-6" /> {/* Alterado para XMarkIcon */}
</button> </button>
</div> </div>
@@ -296,24 +268,19 @@ export function AtendimentoForm({
{searchResults.length > 0 && ( {searchResults.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg"> <div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg">
{searchResults.map((client) => ( {searchResults.map(client => (
<div <div
key={client.id} key={client.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer" className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => { onClick={() => {
setSelectedClient(client); setSelectedClient(client);
setFormData((prev) => ({ setFormData(prev => ({ ...prev, clienteId: client.id }));
...prev,
clienteId: client.id,
}));
setSearchTerm(client.nome); setSearchTerm(client.nome);
setSearchResults([]); setSearchResults([]);
}} }}
> >
<div className="font-medium">{client.nome}</div> <div className="font-medium">{client.nome}</div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">{client.cpfCnpj}</div>
{client.cpfCnpj}
</div>
</div> </div>
))} ))}
</div> </div>
@@ -327,17 +294,12 @@ export function AtendimentoForm({
</label> </label>
<select <select
value={formData.tipo} value={formData.tipo}
onChange={(e) => onChange={(e) => setFormData({ ...formData, tipo: e.target.value as TipoAtendimento })}
setFormData({
...formData,
tipo: e.target.value as TipoAtendimento,
})
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
{Object.values(TipoAtendimento).map((tipo) => ( {Object.values(TipoAtendimento).map((tipo) => (
<option key={tipo} value={tipo}> <option key={tipo} value={tipo}>
{tipo.replace("_", " ")} {tipo.replace('_', ' ')}
</option> </option>
))} ))}
</select> </select>
@@ -351,12 +313,7 @@ export function AtendimentoForm({
</label> </label>
<select <select
value={formData.atendimentoInicialId} value={formData.atendimentoInicialId}
onChange={(e) => onChange={(e) => setFormData({ ...formData, atendimentoInicialId: e.target.value })}
setFormData({
...formData,
atendimentoInicialId: e.target.value,
})
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required={formData.tipo !== TipoAtendimento.INICIAL} required={formData.tipo !== TipoAtendimento.INICIAL}
> >
@@ -374,9 +331,7 @@ export function AtendimentoForm({
fullWidth fullWidth
label="Título do Atendimento" label="Título do Atendimento"
value={formData.titulo} value={formData.titulo}
onChange={(e) => onChange={(e) => setFormData({ ...formData, titulo: e.target.value })}
setFormData({ ...formData, titulo: e.target.value })
}
required required
/> />
@@ -390,14 +345,14 @@ export function AtendimentoForm({
<button <button
type="button" type="button"
onClick={() => editor?.chain().focus().toggleBold().run()} onClick={() => editor?.chain().focus().toggleBold().run()}
className={`px-2 py-1 rounded ${editor?.isActive("bold") ? "bg-gray-200" : "hover:bg-gray-100"}`} className={`px-2 py-1 rounded ${editor?.isActive('bold') ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
> >
Negrito Negrito
</button> </button>
<button <button
type="button" type="button"
onClick={() => editor?.chain().focus().toggleItalic().run()} onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`px-2 py-1 rounded ${editor?.isActive("italic") ? "bg-gray-200" : "hover:bg-gray-100"}`} className={`px-2 py-1 rounded ${editor?.isActive('italic') ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
> >
Itálico Itálico
</button> </button>
@@ -416,16 +371,14 @@ export function AtendimentoForm({
<div <div
{...getRootProps()} {...getRootProps()}
className={`mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md ${ className={`mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md ${
isDragActive ? "border-blue-400 bg-blue-50" : "border-gray-300" isDragActive ? 'border-blue-400 bg-blue-50' : 'border-gray-300'
}`} }`}
> >
<div className="space-y-1 text-center"> <div className="space-y-1 text-center">
<PaperClipIcon className="mx-auto h-12 w-12 text-gray-400" /> <PaperClipIcon className="mx-auto h-12 w-12 text-gray-400" />
<div className="flex text-sm text-gray-600"> <div className="flex text-sm text-gray-600">
<input {...getInputProps()} /> <input {...getInputProps()} />
<p className="pl-1"> <p className="pl-1">Arraste arquivos aqui ou clique para selecionar</p>
Arraste arquivos aqui ou clique para selecionar
</p>
</div> </div>
<p className="text-xs text-gray-500">Máximo 10MB por arquivo</p> <p className="text-xs text-gray-500">Máximo 10MB por arquivo</p>
</div> </div>
@@ -435,26 +388,17 @@ export function AtendimentoForm({
{/* Lista de Anexos */} {/* Lista de Anexos */}
{anexos.length > 0 && ( {anexos.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<h4 className="text-sm font-medium text-gray-700"> <h4 className="text-sm font-medium text-gray-700">Arquivos para upload</h4>
Arquivos para upload
</h4>
<ul className="mt-2 divide-y divide-gray-200"> <ul className="mt-2 divide-y divide-gray-200">
{anexos.map((anexo, index) => ( {anexos.map((anexo, index) => (
<li <li key={index} className="py-3 flex justify-between items-center">
key={index}
className="py-3 flex justify-between items-center"
>
<div className="flex items-center"> <div className="flex items-center">
<PaperClipIcon className="h-5 w-5 text-gray-400 mr-2" /> <PaperClipIcon className="h-5 w-5 text-gray-400 mr-2" />
<span className="text-sm text-gray-900"> <span className="text-sm text-gray-900">{anexo.nome}</span>
{anexo.nome}
</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
{anexo.erro && ( {anexo.erro && (
<span className="text-xs text-red-500 mr-2"> <span className="text-xs text-red-500 mr-2">{anexo.erro}</span>
{anexo.erro}
</span>
)} )}
<button <button
type="button" type="button"
@@ -490,7 +434,7 @@ export function AtendimentoForm({
disabled={isSaving} disabled={isSaving}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{isSaving ? "Salvando..." : "Salvar"} {isSaving ? 'Salvando...' : 'Salvar'}
</button> </button>
</div> </div>
</form> </form>
@@ -1,59 +1,46 @@
import { import { useState } from 'react';
TextField, import { useQuery } from 'react-query';
FormControl, import { useNavigate } from 'react-router-dom';
InputLabel, import api from '../../services/api';
Select, import { AtendimentoForm } from './AtendimentoForm';
MenuItem, import { AgendamentoCalendario } from './AgendamentoCalendario'; // Adicionar esta importação
} from "@mui/material"; import { formatRelative } from 'date-fns';
import { DatePicker } from "@mui/x-date-pickers"; import { ptBR } from 'date-fns/locale';
import { TipoAtendimento } from "@prisma/client"; import { TextField, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import { formatRelative } from "date-fns"; import { DatePicker } from '@mui/x-date-pickers';
import { ptBR } from "date-fns/locale"; import { TipoAtendimento } from '@prisma/client';
import { useState } from "react";
import { useQuery } from "react-query";
import { useNavigate } from "react-router-dom";
import api from "../../services/api";
import { AgendamentoCalendario } from "./AgendamentoCalendario"; // Adicionar esta importação
import { AtendimentoForm } from "./AtendimentoForm";
export const AtendimentosPage = () => { export const AtendimentosPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [formOpen, setFormOpen] = useState(false); const [formOpen, setFormOpen] = useState(false);
const [filtros, setFiltros] = useState({ const [filtros, setFiltros] = useState({
protocolo: "", protocolo: '',
dataInicio: null as Date | null, dataInicio: null,
dataFim: null as Date | null, dataFim: null,
tipo: "", tipo: '',
clienteId: "", clienteId: '',
processoId: "", processoId: '',
}); });
const { data: atendimentos, refetch } = useQuery( const { data: atendimentos, refetch } = useQuery(['atendimentos', filtros], async () => {
["atendimentos", filtros],
async () => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filtros.protocolo) params.append("protocolo", filtros.protocolo); if (filtros.protocolo) params.append('protocolo', filtros.protocolo);
if (filtros.dataInicio) if (filtros.dataInicio) params.append('dataInicio', filtros.dataInicio.toISOString());
params.append("dataInicio", filtros.dataInicio.toISOString()); if (filtros.dataFim) params.append('dataFim', filtros.dataFim.toISOString());
if (filtros.dataFim) if (filtros.tipo) params.append('tipo', filtros.tipo);
params.append("dataFim", filtros.dataFim.toISOString()); if (filtros.clienteId) params.append('clienteId', filtros.clienteId);
if (filtros.tipo) params.append("tipo", filtros.tipo); if (filtros.processoId) params.append('processoId', filtros.processoId);
if (filtros.clienteId) params.append("clienteId", filtros.clienteId);
if (filtros.processoId) params.append("processoId", filtros.processoId);
const response = await api.get(`/atendimentos?${params}`); const response = await api.get(`/atendimentos?${params}`);
return response.data; return response.data;
}, });
);
const handleSave = async () => { const handleSave = async (data: any) => {
try { try {
await refetch(); await refetch();
setFormOpen(false); setFormOpen(false);
} catch (error) { } catch (error) {
console.error("Erro ao atualizar lista:", error); console.error('Erro ao atualizar lista:', error);
} }
}; };
@@ -61,9 +48,7 @@ export const AtendimentosPage = () => {
<div className="p-6 max-w-7xl mx-auto"> <div className="p-6 max-w-7xl mx-auto">
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white"> <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Atendimentos</h1>
Atendimentos
</h1>
<button <button
onClick={() => setFormOpen(true)} onClick={() => setFormOpen(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 transition-colors" className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 transition-colors"
@@ -83,9 +68,7 @@ export const AtendimentosPage = () => {
<TextField <TextField
label="Protocolo" label="Protocolo"
value={filtros.protocolo} value={filtros.protocolo}
onChange={(e) => onChange={(e) => setFiltros({ ...filtros, protocolo: e.target.value })}
setFiltros({ ...filtros, protocolo: e.target.value })
}
size="small" size="small"
/> />
@@ -93,14 +76,14 @@ export const AtendimentosPage = () => {
label="Data Início" label="Data Início"
value={filtros.dataInicio} value={filtros.dataInicio}
onChange={(date) => setFiltros({ ...filtros, dataInicio: date })} onChange={(date) => setFiltros({ ...filtros, dataInicio: date })}
slotProps={{ textField: { size: "small" } }} slotProps={{ textField: { size: 'small' } }}
/> />
<DatePicker <DatePicker
label="Data Fim" label="Data Fim"
value={filtros.dataFim} value={filtros.dataFim}
onChange={(date) => setFiltros({ ...filtros, dataFim: date })} onChange={(date) => setFiltros({ ...filtros, dataFim: date })}
slotProps={{ textField: { size: "small" } }} slotProps={{ textField: { size: 'small' } }}
/> />
<FormControl size="small"> <FormControl size="small">
@@ -108,14 +91,12 @@ export const AtendimentosPage = () => {
<Select <Select
value={filtros.tipo} value={filtros.tipo}
label="Tipo" label="Tipo"
onChange={(e) => onChange={(e) => setFiltros({ ...filtros, tipo: e.target.value })}
setFiltros({ ...filtros, tipo: e.target.value })
}
> >
<MenuItem value="">Todos</MenuItem> <MenuItem value="">Todos</MenuItem>
{Object.values(TipoAtendimento).map((tipo) => ( {Object.values(TipoAtendimento).map((tipo) => (
<MenuItem key={tipo} value={tipo}> <MenuItem key={tipo} value={tipo}>
{tipo.replace("_", " ")} {tipo.replace('_', ' ')}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@@ -128,19 +109,15 @@ export const AtendimentosPage = () => {
<div <div
key={atendimento.id} key={atendimento.id}
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-6 cursor-pointer hover:shadow-lg transition-shadow ${ className={`bg-white dark:bg-gray-800 rounded-lg shadow p-6 cursor-pointer hover:shadow-lg transition-shadow ${
atendimento.status === "CANCELADO" atendimento.status === 'CANCELADO' ? 'border-l-4 border-red-500' : ''
? "border-l-4 border-red-500"
: ""
}`} }`}
onClick={() => navigate(`/atendimentos/${atendimento.id}`)} onClick={() => navigate(`/atendimentos/${atendimento.id}`)}
> >
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-lg font-medium text-gray-900 dark:text-white"> <h2 className="text-lg font-medium text-gray-900 dark:text-white">{atendimento.titulo}</h2>
{atendimento.titulo} {atendimento.status === 'CANCELADO' && (
</h2>
{atendimento.status === "CANCELADO" && (
<span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full"> <span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">
Cancelado Cancelado
</span> </span>
@@ -150,48 +127,27 @@ export const AtendimentosPage = () => {
Protocolo: {atendimento.protocolo} Protocolo: {atendimento.protocolo}
</p> </p>
</div> </div>
<span <span className={`px-3 py-1 rounded-full text-sm font-medium ${
className={`px-3 py-1 rounded-full text-sm font-medium ${ atendimento.tipo === 'INICIAL' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
atendimento.tipo === "INICIAL" }`}>
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
}`}
>
{atendimento.tipo} {atendimento.tipo}
</span> </span>
</div> </div>
<div <div className="mt-4 prose max-w-none" dangerouslySetInnerHTML={{ __html: atendimento.conteudo }} />
className="mt-4 prose max-w-none"
dangerouslySetInnerHTML={{ __html: atendimento.conteudo }}
/>
<div className="mt-4 flex justify-between items-center text-sm text-gray-500 dark:text-gray-400"> <div className="mt-4 flex justify-between items-center text-sm text-gray-500 dark:text-gray-400">
<span> <span>
{formatRelative( {formatRelative(new Date(atendimento.dataAtendimento), new Date(), { locale: ptBR })}
new Date(atendimento.dataAtendimento),
new Date(),
{ locale: ptBR },
)}
</span> </span>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{atendimento.status === "CANCELADO" && ( {atendimento.status === 'CANCELADO' && (
<span className="text-red-600 text-sm"> <span className="text-red-600 text-sm">
Motivo: {atendimento.justificativaCancelamento} Motivo: {atendimento.justificativaCancelamento}
</span> </span>
)} )}
{atendimento.anexos?.length > 0 && ( {atendimento.anexos?.length > 0 && (
<span className="flex items-center"> <span className="flex items-center">
<svg <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
className="w-4 h-4 mr-1" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
/>
</svg> </svg>
{atendimento.anexos.length} anexos {atendimento.anexos.length} anexos
</span> </span>
+54 -71
View File
@@ -1,10 +1,9 @@
import { format } from "date-fns"; import { useEffect, useState } from 'react';
import { ptBR } from "date-fns/locale"; import { Link, useNavigate } from 'react-router-dom';
import { useEffect, useState } from "react"; import { format } from 'date-fns';
import { Link, useNavigate } from "react-router-dom"; import ptBR from 'date-fns/locale/pt-BR';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import { useAuth } from '../../contexts/AuthContext';
import { useAuth } from "../../contexts/AuthContext";
interface Calculo { interface Calculo {
id: string; id: string;
@@ -33,10 +32,10 @@ export function CalculosList() {
const [calculos, setCalculos] = useState<Calculo[]>([]); const [calculos, setCalculos] = useState<Calculo[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filtro, setFiltro] = useState({ const [filtro, setFiltro] = useState({
titulo: "", titulo: '',
status: "", status: '',
processoId: "", processoId: '',
clienteId: "", clienteId: ''
}); });
useEffect(() => { useEffect(() => {
@@ -45,7 +44,7 @@ export function CalculosList() {
carregarCalculos(); carregarCalculos();
} else { } else {
// Redirecionar para login caso não esteja autenticado // Redirecionar para login caso não esteja autenticado
navigate("/login"); navigate('/login');
} }
}, [isAuthenticated, navigate]); }, [isAuthenticated, navigate]);
@@ -55,49 +54,46 @@ export function CalculosList() {
// Construir query string baseado nos filtros // Construir query string baseado nos filtros
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();
if (filtro.titulo) queryParams.append("titulo", filtro.titulo); if (filtro.titulo) queryParams.append('titulo', filtro.titulo);
if (filtro.status) queryParams.append("status", filtro.status); if (filtro.status) queryParams.append('status', filtro.status);
if (filtro.processoId) if (filtro.processoId) queryParams.append('processoId', filtro.processoId);
queryParams.append("processoId", filtro.processoId); if (filtro.clienteId) queryParams.append('clienteId', filtro.clienteId);
if (filtro.clienteId) queryParams.append("clienteId", filtro.clienteId);
// Obter token de autenticação usando o contexto // Obter token de autenticação usando o contexto
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
throw new Error("Usuário não autenticado. Faça login novamente."); throw new Error('Usuário não autenticado. Faça login novamente.');
} }
// Incluir o token no cabeçalho da requisição // Incluir o token no cabeçalho da requisição
const response = await fetch(`/api/calculos?${queryParams.toString()}`, { const response = await fetch(`/api/calculos?${queryParams.toString()}`, {
headers: { headers: {
Authorization: `Bearer ${token}`, 'Authorization': `Bearer ${token}`
}, }
}); });
if (!response.ok) { if (!response.ok) {
if (response.status === 401) { if (response.status === 401) {
// Se receber 401, o token pode estar expirado // Se receber 401, o token pode estar expirado
navigate("/login"); navigate('/login');
throw new Error("Sessão expirada. Por favor, faça login novamente."); throw new Error('Sessão expirada. Por favor, faça login novamente.');
} }
throw new Error("Erro ao carregar cálculos"); throw new Error('Erro ao carregar cálculos');
} }
const data = await response.json(); const data = await response.json();
setCalculos(data); setCalculos(data);
} catch (error) { } catch (error) {
console.error("Erro ao carregar cálculos:", error); console.error('Erro ao carregar cálculos:', error);
toast.error(`Falha ao carregar os cálculos: ${error.message}`); toast.error(`Falha ao carregar os cálculos: ${error.message}`);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleFiltroChange = ( const handleFiltroChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFiltro((prev) => ({ ...prev, [name]: value })); setFiltro(prev => ({ ...prev, [name]: value }));
}; };
const handleSubmitFiltro = (e: React.FormEvent) => { const handleSubmitFiltro = (e: React.FormEvent) => {
@@ -107,18 +103,18 @@ export function CalculosList() {
// Função para formatar valores monetários // Função para formatar valores monetários
const formatarValor = (valor: string) => { const formatarValor = (valor: string) => {
return new Intl.NumberFormat("pt-BR", { return new Intl.NumberFormat('pt-BR', {
style: "currency", style: 'currency',
currency: "BRL", currency: 'BRL'
}).format(parseFloat(valor)); }).format(parseFloat(valor));
}; };
// Função para formatar datas // Função para formatar datas
const formatarData = (data: string) => { const formatarData = (data: string) => {
try { try {
return format(new Date(data), "dd/MM/yyyy", { locale: ptBR }); return format(new Date(data), 'dd/MM/yyyy', { locale: ptBR });
} catch (error) { } catch (error) {
return "Data inválida"; return 'Data inválida';
} }
}; };
@@ -126,21 +122,19 @@ export function CalculosList() {
const StatusBadge = ({ status }: { status: string }) => { const StatusBadge = ({ status }: { status: string }) => {
const getStatusColor = () => { const getStatusColor = () => {
switch (status) { switch (status) {
case "RASCUNHO": case 'RASCUNHO':
return "bg-yellow-100 text-yellow-800"; return 'bg-yellow-100 text-yellow-800';
case "FINALIZADO": case 'FINALIZADO':
return "bg-green-100 text-green-800"; return 'bg-green-100 text-green-800';
case "ARQUIVADO": case 'ARQUIVADO':
return "bg-gray-100 text-gray-800"; return 'bg-gray-100 text-gray-800';
default: default:
return "bg-blue-100 text-blue-800"; return 'bg-blue-100 text-blue-800';
} }
}; };
return ( return (
<span <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor()}`}>
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor()}`}
>
{status} {status}
</span> </span>
); );
@@ -150,26 +144,28 @@ export function CalculosList() {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Cálculos Judiciais</h1> <h1 className="text-2xl font-bold text-gray-900">Cálculos Judiciais</h1>
<div className="flex gap-3">
<Link <Link
to="/calculos/novo" to="/calculos/novo"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
Novo Cálculo Novo Cálculo Judicial
</Link> </Link>
<Link
to="/pensao-alimenticia/novo"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Novo Cálculo de Pensão
</Link>
</div>
</div> </div>
{/* Filtros */} {/* Filtros */}
<div className="bg-white shadow rounded-lg mb-6 p-4"> <div className="bg-white shadow rounded-lg mb-6 p-4">
<h2 className="text-lg font-medium text-gray-700 mb-3">Filtros</h2> <h2 className="text-lg font-medium text-gray-700 mb-3">Filtros</h2>
<form <form onSubmit={handleSubmitFiltro} className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
onSubmit={handleSubmitFiltro}
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<div> <div>
<label <label htmlFor="titulo" className="block text-sm font-medium text-gray-700">
htmlFor="titulo"
className="block text-sm font-medium text-gray-700"
>
Título Título
</label> </label>
<input <input
@@ -183,10 +179,7 @@ export function CalculosList() {
</div> </div>
<div> <div>
<label <label htmlFor="status" className="block text-sm font-medium text-gray-700">
htmlFor="status"
className="block text-sm font-medium text-gray-700"
>
Status Status
</label> </label>
<select <select
@@ -217,18 +210,13 @@ export function CalculosList() {
{/* Lista de Cálculos */} {/* Lista de Cálculos */}
{loading ? ( {loading ? (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div <div className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600" role="status">
className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600"
role="status"
>
<span className="visually-hidden">Carregando...</span> <span className="visually-hidden">Carregando...</span>
</div> </div>
</div> </div>
) : calculos.length === 0 ? ( ) : calculos.length === 0 ? (
<div className="bg-white shadow rounded-lg p-8 text-center"> <div className="bg-white shadow rounded-lg p-8 text-center">
<h3 className="text-lg font-medium text-gray-900"> <h3 className="text-lg font-medium text-gray-900">Nenhum cálculo encontrado</h3>
Nenhum cálculo encontrado
</h3>
<p className="mt-2 text-sm text-gray-500"> <p className="mt-2 text-sm text-gray-500">
Para criar um novo cálculo, clique no botão "Novo Cálculo". Para criar um novo cálculo, clique no botão "Novo Cálculo".
</p> </p>
@@ -238,10 +226,7 @@ export function CalculosList() {
<ul className="divide-y divide-gray-200"> <ul className="divide-y divide-gray-200">
{calculos.map((calculo) => ( {calculos.map((calculo) => (
<li key={calculo.id}> <li key={calculo.id}>
<Link <Link to={`/calculos/${calculo.id}`} className="block hover:bg-gray-50">
to={`/calculos/${calculo.id}`}
className="block hover:bg-gray-50"
>
<div className="px-4 py-4 sm:px-6"> <div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center">
@@ -267,16 +252,14 @@ export function CalculosList() {
</div> </div>
<div className="mt-2 flex items-center text-sm text-gray-500 sm:mt-0"> <div className="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
<p> <p>
Criado em: {formatarData(calculo.criadoEm)} por{" "} Criado em: {formatarData(calculo.criadoEm)} por {calculo.usuario.nome}
{calculo.usuario.nome}
</p> </p>
</div> </div>
</div> </div>
{calculo.processo && ( {calculo.processo && (
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Processo: {calculo.processo.numero} -{" "} Processo: {calculo.processo.numero} - {calculo.processo.titulo}
{calculo.processo.titulo}
</p> </p>
</div> </div>
)} )}
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { useParams } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom'; // Adicionar useNavigate
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import api from '../../services/api';
import api from "../../services/api";
interface CalculoPensao { interface CalculoPensao {
id: string; id: string;
@@ -28,17 +27,36 @@ interface CalculoPensao {
export function DetalhePensaoAlimenticia() { export function DetalhePensaoAlimenticia() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); // Adicionar hook de navegação
const [calculo, setCalculo] = useState<CalculoPensao | null>(null); const [calculo, setCalculo] = useState<CalculoPensao | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Verificação extra para garantir que o id está definido
if (!id || id === "novo") {
setLoading(false);
setCalculo(null);
return;
}
const loadCalculo = async () => { const loadCalculo = async () => {
try { try {
const response = await api.get(`/pensao-alimenticia/${id}`); console.log('[DetalhePensaoAlimenticia] Buscando cálculo para id:', id);
const response = await api.get(`/api/calculos/pensao-alimenticia/${id}`);
setCalculo(response.data); setCalculo(response.data);
} catch (error) { } catch (error: any) {
console.error("Erro ao carregar cálculo:", error); // Log detalhado para depuração
toast.error("Falha ao carregar dados do cálculo"); console.error('[DetalhePensaoAlimenticia] Erro ao carregar cálculo:', error);
// Tratamento específico para erro 404 (cálculo não encontrado)
if (error?.response?.status === 404) {
toast.warn('Cálculo de pensão alimentícia não encontrado.');
} else {
toast.error(
error?.response?.data?.message ||
'Falha ao buscar cálculo de pensão alimentícia.'
);
}
setCalculo(null);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -52,7 +70,17 @@ export function DetalhePensaoAlimenticia() {
} }
if (!calculo) { if (!calculo) {
return <div>Cálculo não encontrado</div>; return (
<div className="bg-white shadow-md rounded-lg p-6 flex flex-col items-center">
<div className="text-lg mb-4 text-gray-700">Nenhum cálculo de pensão alimentícia encontrado.</div>
<button
className="px-4 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
onClick={() => navigate('/pensao-alimenticia/novo')}
>
Criar novo cálculo
</button>
</div>
);
} }
return ( return (
@@ -63,48 +91,24 @@ export function DetalhePensaoAlimenticia() {
<div> <div>
<h2 className="text-lg font-semibold mb-2">Dados do Cálculo</h2> <h2 className="text-lg font-semibold mb-2">Dados do Cálculo</h2>
<div className="space-y-2"> <div className="space-y-2">
<p> <p><span className="font-medium">Data Inicial:</span> {new Date(calculo.dataInicial).toLocaleDateString()}</p>
<span className="font-medium">Data Inicial:</span>{" "}
{new Date(calculo.dataInicial).toLocaleDateString()}
</p>
{calculo.dataFinal && ( {calculo.dataFinal && (
<p> <p><span className="font-medium">Data Final:</span> {new Date(calculo.dataFinal).toLocaleDateString()}</p>
<span className="font-medium">Data Final:</span>{" "}
{new Date(calculo.dataFinal).toLocaleDateString()}
</p>
)} )}
<p> <p><span className="font-medium">Percentual:</span> {calculo.percentualSalarioMinimo}%</p>
<span className="font-medium">Percentual:</span>{" "}
{calculo.percentualSalarioMinimo}%
</p>
{calculo.multaPactuada && ( {calculo.multaPactuada && (
<p> <p><span className="font-medium">Multa:</span> {calculo.multaPactuada}%</p>
<span className="font-medium">Multa:</span>{" "}
{calculo.multaPactuada}%
</p>
)} )}
<p> <p><span className="font-medium">Índice:</span> {calculo.tipoAtualizacao}</p>
<span className="font-medium">Índice:</span>{" "}
{calculo.tipoAtualizacao}
</p>
</div> </div>
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold mb-2">Valores</h2> <h2 className="text-lg font-semibold mb-2">Valores</h2>
<div className="space-y-2"> <div className="space-y-2">
<p> <p><span className="font-medium">Valor Base:</span> R$ {calculo.valorCalculado.toFixed(2)}</p>
<span className="font-medium">Valor Base:</span> R${" "} <p><span className="font-medium">Valor Multa:</span> R$ {calculo.valorMulta.toFixed(2)}</p>
{calculo.valorCalculado.toFixed(2)} <p><span className="font-medium">Valor Total:</span> R$ {calculo.valorTotal.toFixed(2)}</p>
</p>
<p>
<span className="font-medium">Valor Multa:</span> R${" "}
{calculo.valorMulta.toFixed(2)}
</p>
<p>
<span className="font-medium">Valor Total:</span> R${" "}
{calculo.valorTotal.toFixed(2)}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -115,6 +119,15 @@ export function DetalhePensaoAlimenticia() {
<p className="text-gray-700">{calculo.observacoes}</p> <p className="text-gray-700">{calculo.observacoes}</p>
</div> </div>
)} )}
<div className="mt-8 flex justify-end">
<button
className="px-4 py-2 bg-blue-600 text-white rounded-md shadow hover:bg-blue-700"
onClick={() => navigate('/pensao-alimenticia/novo')}
>
Criar novo cálculo
</button>
</div>
</div> </div>
); );
} }
File diff suppressed because it is too large. Load diff
+51 -101
View File
@@ -1,9 +1,8 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import { formatarMoeda, parseMoeda } from '../../utils/formatters';
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from '../../contexts/AuthContext';
import { formatarMoeda, parseMoeda } from "../../utils/formatters";
interface ProcessoOption { interface ProcessoOption {
id: string; id: string;
@@ -22,12 +21,12 @@ export function NovoCalculo() {
const { getToken } = useAuth(); const { getToken } = useAuth();
const valorBaseRef = useRef<HTMLInputElement>(null); const valorBaseRef = useRef<HTMLInputElement>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
titulo: "", titulo: '',
descricao: "", descricao: '',
dataBase: new Date().toISOString().split("T")[0], dataBase: new Date().toISOString().split('T')[0],
valorBase: "", valorBase: '',
processoId: "", processoId: '',
clienteId: "", clienteId: '',
}); });
const [processos, setProcessos] = useState<ProcessoOption[]>([]); const [processos, setProcessos] = useState<ProcessoOption[]>([]);
@@ -38,34 +37,30 @@ export function NovoCalculo() {
const carregarDados = async () => { const carregarDados = async () => {
try { try {
// Carregar processos // Carregar processos
const processosResponse = await fetch("/api/processos"); const processosResponse = await fetch('/api/processos');
if (processosResponse.ok) { if (processosResponse.ok) {
const processosData = await processosResponse.json(); const processosData = await processosResponse.json();
setProcessos(processosData); setProcessos(processosData);
} }
// Carregar clientes // Carregar clientes
const clientesResponse = await fetch("/api/clientes"); const clientesResponse = await fetch('/api/clientes');
if (clientesResponse.ok) { if (clientesResponse.ok) {
const clientesData = await clientesResponse.json(); const clientesData = await clientesResponse.json();
setClientes(clientesData); setClientes(clientesData);
} }
} catch (error) { } catch (error) {
console.error("Erro ao carregar dados:", error); console.error('Erro ao carregar dados:', error);
toast.error("Falha ao carregar dados auxiliares"); toast.error('Falha ao carregar dados auxiliares');
} }
}; };
carregarDados(); carregarDados();
}, []); }, []);
const handleChange = ( const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value })); setFormData(prev => ({ ...prev, [name]: value }));
}; };
// Handler específico para campos monetários com controle de cursor // Handler específico para campos monetários com controle de cursor
@@ -77,22 +72,19 @@ export function NovoCalculo() {
const prevLength = e.target.value.length; const prevLength = e.target.value.length;
// Remover caracteres não numéricos, exceto vírgula e ponto // Remover caracteres não numéricos, exceto vírgula e ponto
const numericValue = value.replace(/[^\d.,]/g, ""); const numericValue = value.replace(/[^\d.,]/g, '');
// Formatar como moeda // Formatar como moeda
const formattedValue = formatarMoeda(numericValue); const formattedValue = formatarMoeda(numericValue);
setFormData((prev) => ({ ...prev, [name]: formattedValue })); setFormData(prev => ({ ...prev, [name]: formattedValue }));
// Usar setTimeout para ajustar a posição do cursor após a atualização do DOM // Usar setTimeout para ajustar a posição do cursor após a atualização do DOM
setTimeout(() => { setTimeout(() => {
if (valorBaseRef.current) { if (valorBaseRef.current) {
// Calcular a nova posição considerando a mudança no comprimento // Calcular a nova posição considerando a mudança no comprimento
const lengthDifference = formattedValue.length - prevLength; const lengthDifference = formattedValue.length - prevLength;
const newPosition = Math.max( const newPosition = Math.max(0, (cursorPosition || 0) + lengthDifference);
0,
(cursorPosition || 0) + lengthDifference,
);
// Definir o cursor na posição calculada // Definir o cursor na posição calculada
valorBaseRef.current.setSelectionRange(newPosition, newPosition); valorBaseRef.current.setSelectionRange(newPosition, newPosition);
@@ -107,7 +99,7 @@ export function NovoCalculo() {
try { try {
// Validar dados // Validar dados
if (!formData.titulo || !formData.dataBase || !formData.valorBase) { if (!formData.titulo || !formData.dataBase || !formData.valorBase) {
toast.error("Preencha todos os campos obrigatórios"); toast.error('Preencha todos os campos obrigatórios');
setLoading(false); setLoading(false);
return; return;
} }
@@ -115,7 +107,7 @@ export function NovoCalculo() {
// Converter valor para número // Converter valor para número
const valorBaseNumerico = parseMoeda(formData.valorBase); const valorBaseNumerico = parseMoeda(formData.valorBase);
if (isNaN(valorBaseNumerico) || valorBaseNumerico <= 0) { if (isNaN(valorBaseNumerico) || valorBaseNumerico <= 0) {
toast.error("Valor base inválido. Informe um valor maior que zero"); toast.error('Valor base inválido. Informe um valor maior que zero');
setLoading(false); setLoading(false);
return; return;
} }
@@ -123,47 +115,45 @@ export function NovoCalculo() {
// Preparar dados para envio // Preparar dados para envio
const dadosParaEnviar = { const dadosParaEnviar = {
...formData, ...formData,
valorBase: valorBaseNumerico, valorBase: valorBaseNumerico
}; };
// Obter token de autenticação usando o contexto // Obter token de autenticação usando o contexto
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
throw new Error("Usuário não autenticado. Faça login novamente."); throw new Error('Usuário não autenticado. Faça login novamente.');
} }
// Enviar solicitação para criar cálculo com cabeçalho de autorização // Enviar solicitação para criar cálculo com cabeçalho de autorização
const response = await fetch("/api/calculos", { const response = await fetch('/api/calculos', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`, 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify(dadosParaEnviar), body: JSON.stringify(dadosParaEnviar),
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Erro ao criar cálculo"); throw new Error(errorData.error || 'Erro ao criar cálculo');
} }
const resultado = await response.json(); const resultado = await response.json();
toast.success("Cálculo criado com sucesso!"); toast.success('Cálculo criado com sucesso!');
// Redirecionar para a página do cálculo criado // Redirecionar para a página do cálculo criado
navigate(`/calculos/${resultado.id}`); navigate(`/calculos/${resultado.id}`);
} catch (error) { } catch (error) {
console.error("Erro ao criar cálculo:", error); console.error('Erro ao criar cálculo:', error);
toast.error(`Falha ao criar cálculo: ${error.message}`); toast.error(`Falha ao criar cálculo: ${error.message}`);
if ( if (error.message?.includes('autenticação') ||
error.message?.includes("autenticação") || error.message?.includes('não autenticado') ||
error.message?.includes("não autenticado") || error.response?.status === 401) {
error.response?.status === 401
) {
// Se for erro de autenticação, redirecionar para login // Se for erro de autenticação, redirecionar para login
navigate("/login"); navigate('/login');
} }
} finally { } finally {
setLoading(false); setLoading(false);
@@ -174,19 +164,14 @@ export function NovoCalculo() {
<div className="max-w-4xl mx-auto bg-white shadow-md rounded-lg overflow-hidden"> <div className="max-w-4xl mx-auto bg-white shadow-md rounded-lg overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4 text-white"> <div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4 text-white">
<h1 className="text-xl font-bold">Novo Cálculo Judicial</h1> <h1 className="text-xl font-bold">Novo Cálculo Judicial</h1>
<p className="text-sm text-blue-100 mt-1"> <p className="text-sm text-blue-100 mt-1">Preencha os dados básicos para criar um novo cálculo</p>
Preencha os dados básicos para criar um novo cálculo
</p>
</div> </div>
<form onSubmit={handleSubmit} className="p-6"> <form onSubmit={handleSubmit} className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Título */} {/* Título */}
<div className="col-span-2"> <div className="col-span-2">
<label <label htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="titulo"
className="block text-sm font-medium text-gray-700 mb-1"
>
Título <span className="text-red-500">*</span> Título <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -203,10 +188,7 @@ export function NovoCalculo() {
{/* Data Base */} {/* Data Base */}
<div> <div>
<label <label htmlFor="dataBase" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="dataBase"
className="block text-sm font-medium text-gray-700 mb-1"
>
Data Base <span className="text-red-500">*</span> Data Base <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -218,17 +200,12 @@ export function NovoCalculo() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required required
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">Data de referência para iniciar o cálculo</p>
Data de referência para iniciar o cálculo
</p>
</div> </div>
{/* Valor Base */} {/* Valor Base */}
<div> <div>
<label <label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="valorBase"
className="block text-sm font-medium text-gray-700 mb-1"
>
Valor Base <span className="text-red-500">*</span> Valor Base <span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
@@ -247,17 +224,12 @@ export function NovoCalculo() {
required required
/> />
</div> </div>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">Valor inicial para o cálculo</p>
Valor inicial para o cálculo
</p>
</div> </div>
{/* Processo */} {/* Processo */}
<div> <div>
<label <label htmlFor="processoId" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="processoId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Processo Processo
</label> </label>
<select <select
@@ -268,7 +240,7 @@ export function NovoCalculo() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
> >
<option value="">Selecione um processo (opcional)</option> <option value="">Selecione um processo (opcional)</option>
{processos.map((processo) => ( {processos.map(processo => (
<option key={processo.id} value={processo.id}> <option key={processo.id} value={processo.id}>
{processo.numero} - {processo.titulo} {processo.numero} - {processo.titulo}
</option> </option>
@@ -278,10 +250,7 @@ export function NovoCalculo() {
{/* Cliente */} {/* Cliente */}
<div> <div>
<label <label htmlFor="clienteId" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="clienteId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Cliente Cliente
</label> </label>
<select <select
@@ -292,7 +261,7 @@ export function NovoCalculo() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
> >
<option value="">Selecione um cliente (opcional)</option> <option value="">Selecione um cliente (opcional)</option>
{clientes.map((cliente) => ( {clientes.map(cliente => (
<option key={cliente.id} value={cliente.id}> <option key={cliente.id} value={cliente.id}>
{cliente.nome} {cliente.nome}
</option> </option>
@@ -302,10 +271,7 @@ export function NovoCalculo() {
{/* Descrição */} {/* Descrição */}
<div className="col-span-2"> <div className="col-span-2">
<label <label htmlFor="descricao" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="descricao"
className="block text-sm font-medium text-gray-700 mb-1"
>
Descrição Descrição
</label> </label>
<textarea <textarea
@@ -323,7 +289,7 @@ export function NovoCalculo() {
<div className="mt-8 flex justify-end space-x-3"> <div className="mt-8 flex justify-end space-x-3">
<button <button
type="button" type="button"
onClick={() => navigate("/calculos")} onClick={() => navigate('/calculos')}
disabled={loading} disabled={loading}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
@@ -335,28 +301,12 @@ export function NovoCalculo() {
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
> >
{loading && ( {loading && (
<svg <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
xmlns="http://www.w3.org/2000/svg" <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
)} )}
{loading ? "Criando..." : "Criar Cálculo"} {loading ? 'Criando...' : 'Criar Cálculo'}
</button> </button>
</div> </div>
</form> </form>
+68 -135
View File
@@ -1,8 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import { useAuth } from '../../contexts/AuthContext';
import { formatarPercentual, parsePercentual } from "../../utils/formatters"; import { formatarPercentual, parsePercentual } from '../../utils/formatters';
interface ProcessoOption { interface ProcessoOption {
id: string; id: string;
@@ -17,18 +17,19 @@ interface ClienteOption {
export function NovoPensaoAlimenticia() { export function NovoPensaoAlimenticia() {
const navigate = useNavigate(); const navigate = useNavigate();
const { getToken } = useAuth();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
titulo: "", titulo: '',
descricao: "", descricao: '',
dataInicial: new Date().toISOString().split("T")[0], dataInicial: new Date().toISOString().split('T')[0],
dataFinal: "", dataFinal: '',
percentualSalarioMinimo: "", percentualSalarioMinimo: '',
multaPactuada: "", multaPactuada: '',
tipoAtualizacao: "INPC", tipoAtualizacao: 'INPC',
processoId: "", processoId: '',
clienteId: "", clienteId: '',
observacoes: "", observacoes: '',
}); });
const [processos, setProcessos] = useState<ProcessoOption[]>([]); const [processos, setProcessos] = useState<ProcessoOption[]>([]);
@@ -39,44 +40,40 @@ export function NovoPensaoAlimenticia() {
const carregarDados = async () => { const carregarDados = async () => {
try { try {
// Carregar processos // Carregar processos
const processosResponse = await fetch("/api/processos"); const processosResponse = await fetch('/api/processos');
if (processosResponse.ok) { if (processosResponse.ok) {
const processosData = await processosResponse.json(); const processosData = await processosResponse.json();
setProcessos(processosData); setProcessos(processosData);
} }
// Carregar clientes // Carregar clientes
const clientesResponse = await fetch("/api/clientes"); const clientesResponse = await fetch('/api/clientes');
if (clientesResponse.ok) { if (clientesResponse.ok) {
const clientesData = await clientesResponse.json(); const clientesData = await clientesResponse.json();
setClientes(clientesData); setClientes(clientesData);
} }
} catch (error) { } catch (error) {
console.error("Erro ao carregar dados:", error); console.error('Erro ao carregar dados:', error);
toast.error("Falha ao carregar dados auxiliares"); toast.error('Falha ao carregar dados auxiliares');
} }
}; };
carregarDados(); carregarDados();
}, []); }, []);
const handleChange = ( const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>,
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value })); setFormData(prev => ({ ...prev, [name]: value }));
}; };
// Handler para campos percentuais // Handler para campos percentuais
const handlePercentChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePercentChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
// Remover caracteres não numéricos, exceto vírgula e ponto // Remover caracteres não numéricos, exceto vírgula e ponto
const numericValue = value.replace(/[^\d.,]/g, ""); const numericValue = value.replace(/[^\d.,]/g, '');
// Formatar como percentual // Formatar como percentual
const formattedValue = formatarPercentual(numericValue); const formattedValue = formatarPercentual(numericValue);
setFormData((prev) => ({ ...prev, [name]: formattedValue })); setFormData(prev => ({ ...prev, [name]: formattedValue }));
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@@ -85,12 +82,8 @@ export function NovoPensaoAlimenticia() {
try { try {
// Validar dados // Validar dados
if ( if (!formData.titulo || !formData.dataInicial || !formData.percentualSalarioMinimo) {
!formData.titulo || toast.error('Preencha todos os campos obrigatórios');
!formData.dataInicial ||
!formData.percentualSalarioMinimo
) {
toast.error("Preencha todos os campos obrigatórios");
setLoading(false); setLoading(false);
return; return;
} }
@@ -100,9 +93,7 @@ export function NovoPensaoAlimenticia() {
// Verificar se percentual é válido // Verificar se percentual é válido
if (isNaN(percentual) || percentual <= 0 || percentual > 1) { if (isNaN(percentual) || percentual <= 0 || percentual > 1) {
toast.error( toast.error('Percentual do salário mínimo deve ser maior que 0 e menor ou igual a 100%');
"Percentual do salário mínimo deve ser maior que 0 e menor ou igual a 100%",
);
setLoading(false); setLoading(false);
return; return;
} }
@@ -111,35 +102,39 @@ export function NovoPensaoAlimenticia() {
const dadosParaEnviar = { const dadosParaEnviar = {
...formData, ...formData,
percentualSalarioMinimo: percentual, percentualSalarioMinimo: percentual,
multaPactuada: formData.multaPactuada multaPactuada: formData.multaPactuada ? parsePercentual(formData.multaPactuada) : undefined,
? parsePercentual(formData.multaPactuada) processoId: formData.processoId || undefined, // Corrigir aqui
: undefined, clienteId: formData.clienteId || undefined, // Corrigir aqui
}; };
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Enviar solicitação para criar cálculo // Enviar solicitação para criar cálculo
const response = await fetch("/api/pensao-alimenticia", { const response = await fetch('/api/pensao-alimenticia', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Adicionar o token aqui
}, },
body: JSON.stringify(dadosParaEnviar), body: JSON.stringify(dadosParaEnviar),
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error( throw new Error(errorData.error || 'Erro ao criar cálculo de pensão alimentícia');
errorData.error || "Erro ao criar cálculo de pensão alimentícia",
);
} }
const resultado = await response.json(); const resultado = await response.json();
toast.success("Cálculo de pensão alimentícia criado com sucesso!"); toast.success('Cálculo de pensão alimentícia criado com sucesso!');
// Redirecionar para a página do cálculo criado // Redirecionar para a página do cálculo criado
navigate(`/pensao-alimenticia/${resultado.id}`); navigate(`/pensao-alimenticia/${resultado.id}`);
} catch (error) { } catch (error) {
console.error("Erro ao criar cálculo de pensão:", error); console.error('Erro ao criar cálculo de pensão:', error);
toast.error(`Falha ao criar cálculo: ${error.message}`); toast.error(`Falha ao criar cálculo: ${error.message}`);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -149,22 +144,15 @@ export function NovoPensaoAlimenticia() {
return ( return (
<div className="max-w-4xl mx-auto bg-white shadow-md rounded-lg overflow-hidden"> <div className="max-w-4xl mx-auto bg-white shadow-md rounded-lg overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4 text-white"> <div className="bg-gradient-to-r from-blue-600 to-blue-800 px-6 py-4 text-white">
<h1 className="text-xl font-bold"> <h1 className="text-xl font-bold">Novo Cálculo de Pensão Alimentícia</h1>
Novo Cálculo de Pensão Alimentícia <p className="text-sm text-blue-100 mt-1">Preencha os dados para criar um novo cálculo de pensão alimentícia</p>
</h1>
<p className="text-sm text-blue-100 mt-1">
Preencha os dados para criar um novo cálculo de pensão alimentícia
</p>
</div> </div>
<form onSubmit={handleSubmit} className="p-6"> <form onSubmit={handleSubmit} className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Título */} {/* Título */}
<div className="col-span-2"> <div className="col-span-2">
<label <label htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="titulo"
className="block text-sm font-medium text-gray-700 mb-1"
>
Título <span className="text-red-500">*</span> Título <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -181,10 +169,7 @@ export function NovoPensaoAlimenticia() {
{/* Índice de Atualização */} {/* Índice de Atualização */}
<div> <div>
<label <label htmlFor="tipoAtualizacao" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="tipoAtualizacao"
className="block text-sm font-medium text-gray-700 mb-1"
>
Índice de Atualização Monetária Índice de Atualização Monetária
</label> </label>
<select <select
@@ -194,29 +179,17 @@ export function NovoPensaoAlimenticia() {
onChange={handleChange} onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
> >
<option value="INPC"> <option value="INPC">INPC (Índice Nacional de Preços ao Consumidor)</option>
INPC (Índice Nacional de Preços ao Consumidor) <option value="IPCA">IPCA (Índice Nacional de Preços ao Consumidor Amplo)</option>
</option> <option value="IGPM">IGP-M (Índice Geral de Preços do Mercado)</option>
<option value="IPCA"> <option value="SELIC">SELIC (Sistema Especial de Liquidação e Custódia)</option>
IPCA (Índice Nacional de Preços ao Consumidor Amplo)
</option>
<option value="IGPM">
IGP-M (Índice Geral de Preços do Mercado)
</option>
<option value="SELIC">
SELIC (Sistema Especial de Liquidação e Custódia)
</option>
</select> </select>
</div> </div>
{/* Percentual do Salário Mínimo */} {/* Percentual do Salário Mínimo */}
<div> <div>
<label <label htmlFor="percentualSalarioMinimo" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="percentualSalarioMinimo" Percentual do Salário Mínimo <span className="text-red-500">*</span>
className="block text-sm font-medium text-gray-700 mb-1"
>
Percentual do Salário Mínimo{" "}
<span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<input <input
@@ -238,10 +211,7 @@ export function NovoPensaoAlimenticia() {
{/* Data Inicial */} {/* Data Inicial */}
<div> <div>
<label <label htmlFor="dataInicial" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="dataInicial"
className="block text-sm font-medium text-gray-700 mb-1"
>
Data Inicial <span className="text-red-500">*</span> Data Inicial <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -253,17 +223,12 @@ export function NovoPensaoAlimenticia() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required required
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">Data de início da pensão</p>
Data de início da pensão
</p>
</div> </div>
{/* Data Final */} {/* Data Final */}
<div> <div>
<label <label htmlFor="dataFinal" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="dataFinal"
className="block text-sm font-medium text-gray-700 mb-1"
>
Data Final Data Final
</label> </label>
<input <input
@@ -274,17 +239,12 @@ export function NovoPensaoAlimenticia() {
onChange={handleChange} onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">Deixe em branco se a pensão não tem data final</p>
Deixe em branco se a pensão não tem data final
</p>
</div> </div>
{/* Multa Pactuada */} {/* Multa Pactuada */}
<div> <div>
<label <label htmlFor="multaPactuada" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="multaPactuada"
className="block text-sm font-medium text-gray-700 mb-1"
>
Multa Pactuada por Atraso Multa Pactuada por Atraso
</label> </label>
<div className="relative"> <div className="relative">
@@ -301,17 +261,12 @@ export function NovoPensaoAlimenticia() {
<span className="text-gray-500 sm:text-sm">%</span> <span className="text-gray-500 sm:text-sm">%</span>
</div> </div>
</div> </div>
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">Ex: 10% = 10,00%. Deixe em branco se não houver.</p>
Ex: 10% = 10,00%. Deixe em branco se não houver.
</p>
</div> </div>
{/* Processo */} {/* Processo */}
<div> <div>
<label <label htmlFor="processoId" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="processoId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Processo Processo
</label> </label>
<select <select
@@ -322,7 +277,7 @@ export function NovoPensaoAlimenticia() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
> >
<option value="">Selecione um processo (opcional)</option> <option value="">Selecione um processo (opcional)</option>
{processos.map((processo) => ( {processos.map(processo => (
<option key={processo.id} value={processo.id}> <option key={processo.id} value={processo.id}>
{processo.numero} - {processo.titulo} {processo.numero} - {processo.titulo}
</option> </option>
@@ -332,10 +287,7 @@ export function NovoPensaoAlimenticia() {
{/* Cliente */} {/* Cliente */}
<div> <div>
<label <label htmlFor="clienteId" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="clienteId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Cliente Cliente
</label> </label>
<select <select
@@ -346,7 +298,7 @@ export function NovoPensaoAlimenticia() {
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
> >
<option value="">Selecione um cliente (opcional)</option> <option value="">Selecione um cliente (opcional)</option>
{clientes.map((cliente) => ( {clientes.map(cliente => (
<option key={cliente.id} value={cliente.id}> <option key={cliente.id} value={cliente.id}>
{cliente.nome} {cliente.nome}
</option> </option>
@@ -356,10 +308,7 @@ export function NovoPensaoAlimenticia() {
{/* Observações */} {/* Observações */}
<div className="col-span-2"> <div className="col-span-2">
<label <label htmlFor="observacoes" className="block text-sm font-medium text-gray-700 mb-1">
htmlFor="observacoes"
className="block text-sm font-medium text-gray-700 mb-1"
>
Observações Observações
</label> </label>
<textarea <textarea
@@ -377,7 +326,7 @@ export function NovoPensaoAlimenticia() {
<div className="mt-8 flex justify-end space-x-3"> <div className="mt-8 flex justify-end space-x-3">
<button <button
type="button" type="button"
onClick={() => navigate("/calculos")} onClick={() => navigate('/calculos')}
disabled={loading} disabled={loading}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
@@ -389,28 +338,12 @@ export function NovoPensaoAlimenticia() {
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center" className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
> >
{loading && ( {loading && (
<svg <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
xmlns="http://www.w3.org/2000/svg" <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
)} )}
{loading ? "Criando..." : "Criar Cálculo de Pensão"} {loading ? 'Criando...' : 'Criar Cálculo de Pensão'}
</button> </button>
</div> </div>
</form> </form>
+4 -6
View File
@@ -1,3 +1,6 @@
import React, { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useClientes } from "../../hooks/useClientes";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { import {
ArrowLeftIcon, ArrowLeftIcon,
@@ -8,10 +11,7 @@ import {
UserGroupIcon, UserGroupIcon,
DocumentTextIcon, DocumentTextIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import React, { useState } from "react"; import { ClienteModal } from "./ClienteModal";
import { useParams, useNavigate } from "react-router-dom";
import { useClientes } from "../../hooks/useClientes";
import { import {
formatCPF, formatCPF,
formatTelefone, formatTelefone,
@@ -22,8 +22,6 @@ import {
formatData, formatData,
} from "../../utils/formatters"; } from "../../utils/formatters";
import { ClienteModal } from "./ClienteModal";
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" "); return classes.filter(Boolean).join(" ");
} }
+40 -67
View File
@@ -1,6 +1,3 @@
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import axios from "axios";
import React, { import React, {
useState, useState,
useEffect, useEffect,
@@ -8,10 +5,12 @@ import React, {
forwardRef, forwardRef,
useRef, useRef,
} from "react"; } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import axios from "axios";
import { useClientes } from "../../hooks/useClientes";
import { Cliente } from "../../types/entities"; import { Cliente } from "../../types/entities";
import { useClientes } from "../../hooks/useClientes";
import { ORGAOS_EMISSORES, PREVIDENCIA_OPTIONS } from "../../utils/constants"; import { ORGAOS_EMISSORES, PREVIDENCIA_OPTIONS } from "../../utils/constants";
const estadosCivis = [ const estadosCivis = [
@@ -202,8 +201,7 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
const cepValue = watch("cep"); const cepValue = watch("cep");
// Função para buscar dados do CEP - versão corrigida // Função para buscar dados do CEP - versão corrigida
const fetchAddressByCep = useCallback( const fetchAddressByCep = useCallback(async (cep: string) => {
async (cep: string) => {
if (!cep || cep.length !== 8) return; if (!cep || cep.length !== 8) return;
try { try {
@@ -214,25 +212,24 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
); );
if (response.data && !response.data.erro) { if (response.data && !response.data.erro) {
// Usar estado local para garantir re-renderização imediata
const endereco = response.data.logradouro || ""; const endereco = response.data.logradouro || "";
const bairro = response.data.bairro || ""; const bairro = response.data.bairro || "";
const cidade = response.data.localidade || ""; const cidade = response.data.localidade || "";
const estado = response.data.uf || ""; const estado = response.data.uf || "";
// Atualizar valores no formulário // Só preencher endereço/bairro se vier valor da API
setValue("endereco", endereco, { shouldValidate: true }); if (endereco) setValue("endereco", endereco, { shouldValidate: true });
setValue("bairro", bairro, { shouldValidate: true }); if (bairro) setValue("bairro", bairro, { shouldValidate: true });
setValue("cidade", cidade, { shouldValidate: true }); setValue("cidade", cidade, { shouldValidate: true });
setValue("estado", estado, { shouldValidate: true }); setValue("estado", estado, { shouldValidate: true });
// Atualizar o estado para re-renderização do componente // Atualizar estado apenas para cidade/estado (não controlar endereço/bairro)
setEnderecoState({ setEnderecoState({
endereco, endereco: endereco,
bairro, bairro: bairro,
cidade, cidade: cidade,
estado, estado: estado,
preenchido: true, preenchido: false, // Não usar mais para travar campos
}); });
// Focar no próximo campo apenas se o usuário não estiver digitando ativamente // Focar no próximo campo apenas se o usuário não estiver digitando ativamente
@@ -240,20 +237,31 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
const activeElement = document.activeElement; const activeElement = document.activeElement;
const isUserTyping = const isUserTyping =
activeElement && activeElement &&
(activeElement.tagName === "INPUT" || (activeElement.tagName === 'INPUT' || activeElement.tagName === 'SELECT') &&
activeElement.tagName === "SELECT") &&
activeElement !== document.querySelector('input[name="cep"]'); activeElement !== document.querySelector('input[name="cep"]');
// Só focar no campo número se o usuário não estiver digitando em outro campo // Só focar no campo número se o usuário não estiver digitando em outro campo
if (!isUserTyping) { if (!isUserTyping) {
setTimeout(() => { setTimeout(() => {
const numeroInput = document.querySelector( const numeroInput = document.querySelector(
'input[name="numero"]', 'input[name="numero"]'
) as HTMLInputElement; ) as HTMLInputElement;
if (numeroInput) numeroInput.focus(); if (numeroInput) numeroInput.focus();
}, 100); }, 100);
} }
} else { } else {
// Se CEP não encontrado, limpar campos e permitir edição manual
setEnderecoState({
endereco: "",
bairro: "",
cidade: "",
estado: "",
preenchido: false,
});
setValue("endereco", "");
setValue("bairro", "");
setValue("cidade", "");
setValue("estado", "");
console.error("CEP não encontrado"); console.error("CEP não encontrado");
alert("CEP não encontrado. Por favor, verifique o CEP informado."); alert("CEP não encontrado. Por favor, verifique o CEP informado.");
} }
@@ -263,9 +271,7 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
} finally { } finally {
setIsLoadingCep(false); setIsLoadingCep(false);
} }
}, }, [setValue]);
[setValue],
);
// Efeito para monitorar mudanças no CEP com debounce otimizado // Efeito para monitorar mudanças no CEP com debounce otimizado
const [autoFetchEnabled, setAutoFetchEnabled] = useState(true); const [autoFetchEnabled, setAutoFetchEnabled] = useState(true);
@@ -589,10 +595,7 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
}} }}
onBlur={(e) => { onBlur={(e) => {
// Forçar busca ao sair do campo // Forçar busca ao sair do campo
const cleanCep = e.target.value.replace( const cleanCep = e.target.value.replace(/\D/g, "");
/\D/g,
"",
);
if (cleanCep && cleanCep.length === 8) { if (cleanCep && cleanCep.length === 8) {
fetchAddressByCep(cleanCep); fetchAddressByCep(cleanCep);
} }
@@ -630,17 +633,9 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
required: "Endereço é obrigatório", required: "Endereço é obrigatório",
})} })}
error={errors.endereco?.message} error={errors.endereco?.message}
value={ // Remover value/controlado por enderecoState, deixar react-hook-form controlar
enderecoState.preenchido
? enderecoState.endereco
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("endereco", e.target.value); setValue("endereco", e.target.value, { shouldValidate: true });
setEnderecoState((prev) => ({
...prev,
endereco: e.target.value,
}));
}} }}
/> />
<FormInput <FormInput
@@ -660,17 +655,9 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
required: "Bairro é obrigatório", required: "Bairro é obrigatório",
})} })}
error={errors.bairro?.message} error={errors.bairro?.message}
value={ // Remover value/controlado por enderecoState, deixar react-hook-form controlar
enderecoState.preenchido
? enderecoState.bairro
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("bairro", e.target.value); setValue("bairro", e.target.value, { shouldValidate: true });
setEnderecoState((prev) => ({
...prev,
bairro: e.target.value,
}));
}} }}
/> />
<FormInput <FormInput
@@ -679,17 +666,10 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
required: "Cidade é obrigatória", required: "Cidade é obrigatória",
})} })}
error={errors.cidade?.message} error={errors.cidade?.message}
value={ value={typeof watch("cidade") === "string" ? watch("cidade") : enderecoState.cidade}
enderecoState.preenchido
? enderecoState.cidade
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("cidade", e.target.value); setValue("cidade", e.target.value, { shouldValidate: true });
setEnderecoState((prev) => ({ setEnderecoState(prev => ({...prev, cidade: e.target.value}));
...prev,
cidade: e.target.value,
}));
}} }}
/> />
<FormSelect <FormSelect
@@ -699,17 +679,10 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
})} })}
options={estadosOptions} options={estadosOptions}
error={errors.estado?.message} error={errors.estado?.message}
value={ value={typeof watch("estado") === "string" ? watch("estado") : enderecoState.estado}
enderecoState.preenchido
? enderecoState.estado
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("estado", e.target.value); setValue("estado", e.target.value, { shouldValidate: true });
setEnderecoState((prev) => ({ setEnderecoState(prev => ({...prev, estado: e.target.value}));
...prev,
estado: e.target.value,
}));
}} }}
/> />
</div> </div>
+18 -23
View File
@@ -1,20 +1,16 @@
import { PencilIcon } from "@heroicons/react/24/outline"; import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from "react"; import { useQuery } from 'react-query';
import { useQuery } from "react-query"; import { useNavigate } from 'react-router-dom';
import { useNavigate } from "react-router-dom"; import { PencilIcon } from '@heroicons/react/24/outline';
import api from '../../services/api';
import api from "../../services/api"; import { formatCPF, formatTelefone } from '../../utils/formatters';
import { formatCPF, formatTelefone } from "../../utils/formatters";
interface ClientesListProps { interface ClientesListProps {
searchTerm: string; searchTerm: string;
onEdit: (cliente: any) => void; onEdit: (cliente: any) => void;
} }
export const ClientesList: React.FC<ClientesListProps> = ({ export const ClientesList: React.FC<ClientesListProps> = ({ searchTerm, onEdit }) => {
searchTerm,
onEdit,
}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [debouncedTerm, setDebouncedTerm] = useState(searchTerm); const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
@@ -28,38 +24,37 @@ export const ClientesList: React.FC<ClientesListProps> = ({
}, [searchTerm]); }, [searchTerm]);
const { data: clientes = [], isLoading } = useQuery( const { data: clientes = [], isLoading } = useQuery(
["clientes", debouncedTerm], ['clientes', debouncedTerm],
async () => { async () => {
const response = await api.get("/clientes", { const response = await api.get('/clientes', {
params: { params: {
search: debouncedTerm.toUpperCase(), search: debouncedTerm.toUpperCase()
}, }
}); });
// Filtrar localmente para melhor resposta // Filtrar localmente para melhor resposta
let filteredClientes = response.data; let filteredClientes = response.data;
if (debouncedTerm) { if (debouncedTerm) {
const searchTerms = debouncedTerm.toUpperCase().split(" "); const searchTerms = debouncedTerm.toUpperCase().split(' ');
filteredClientes = response.data.filter((cliente: any) => filteredClientes = response.data.filter((cliente: any) =>
searchTerms.every( searchTerms.every(term =>
(term) =>
cliente.nome.includes(term) || cliente.nome.includes(term) ||
cliente.cpfCnpj.includes(term) || cliente.cpfCnpj.includes(term) ||
cliente.email?.includes(term.toLowerCase()), cliente.email?.includes(term.toLowerCase())
), )
); );
} }
// Ordenar por nome // Ordenar por nome
return filteredClientes.sort((a: any, b: any) => return filteredClientes.sort((a: any, b: any) =>
a.nome.localeCompare(b.nome), a.nome.localeCompare(b.nome)
); );
}, },
{ {
staleTime: 1000 * 60, // 1 minuto staleTime: 1000 * 60, // 1 minuto
cacheTime: 1000 * 60 * 5, // 5 minutos cacheTime: 1000 * 60 * 5, // 5 minutos
keepPreviousData: true, // Manter dados anteriores enquanto carrega novos keepPreviousData: true // Manter dados anteriores enquanto carrega novos
}, }
); );
if (isLoading) { if (isLoading) {
+3 -5
View File
@@ -1,10 +1,8 @@
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import React, { useState } from "react"; import React, { useState } from "react";
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Cliente } from "../../types/entities";
import { ClienteModal } from "./ClienteModal.tsx";
import { ClientesList } from "./ClientesList.tsx"; import { ClientesList } from "./ClientesList.tsx";
import { ClienteModal } from "./ClienteModal.tsx";
import { Cliente } from "../../types/entities";
export const ClientesPage: React.FC = () => { export const ClientesPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
+13 -20
View File
@@ -1,23 +1,20 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import React from 'react';
import { format, differenceInDays } from "date-fns"; import { useQuery } from 'react-query';
import { ptBR } from "date-fns/locale"; import { format, differenceInDays } from 'date-fns';
import React from "react"; import { ptBR } from 'date-fns/locale';
import { useQuery } from "react-query"; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import api from '../../services/api';
import api from "../../services/api";
export const AlertasPrazos: React.FC = () => { export const AlertasPrazos: React.FC = () => {
const { data: prazos = [] } = useQuery("prazosFuturos", async () => { const { data: prazos = [] } = useQuery('prazosFuturos', async () => {
const response = await api.get("/dashboard/prazos/proximos"); const response = await api.get('/dashboard/prazos/proximos');
return response.data; return response.data;
}); });
const getPrioridadeStyle = (dias: number) => { const getPrioridadeStyle = (dias: number) => {
if (dias <= 3) if (dias <= 3) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; if (dias <= 7) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
if (dias <= 7) return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
}; };
return ( return (
@@ -41,12 +38,8 @@ export const AlertasPrazos: React.FC = () => {
Processo: {prazo.processo.numero} Processo: {prazo.processo.numero}
</p> </p>
</div> </div>
<span <span className={`px-2 py-1 rounded-full text-xs ${getPrioridadeStyle(dias)}`}>
className={`px-2 py-1 rounded-full text-xs ${getPrioridadeStyle(dias)}`} {format(new Date(prazo.dataLimite), 'dd/MM/yyyy', { locale: ptBR })}
>
{format(new Date(prazo.dataLimite), "dd/MM/yyyy", {
locale: ptBR,
})}
</span> </span>
</div> </div>
); );
+12 -17
View File
@@ -1,25 +1,22 @@
import { format } from "date-fns"; import React from 'react';
import { ptBR } from "date-fns/locale"; import { useQuery } from 'react-query';
import React from "react"; import { format } from 'date-fns';
import { useQuery } from "react-query"; import { ptBR } from 'date-fns/locale';
import api from '../../services/api';
import api from "../../services/api";
export const AtividadesRecentes: React.FC = () => { export const AtividadesRecentes: React.FC = () => {
const { data: atividades = [] } = useQuery("atividades", async () => { const { data: atividades = [] } = useQuery('atividades', async () => {
const response = await api.get("/dashboard/atividades"); const response = await api.get('/dashboard/atividades');
return response.data; return response.data;
}); });
const formatData = (data: string | Date | null) => { const formatData = (data: string | Date | null) => {
if (!data) return ""; if (!data) return '';
try { try {
return format(new Date(data), "dd 'de' MMMM 'às' HH:mm", { return format(new Date(data), "dd 'de' MMMM 'às' HH:mm", { locale: ptBR });
locale: ptBR,
});
} catch (error) { } catch (error) {
console.error("Erro ao formatar data:", error); console.error('Erro ao formatar data:', error);
return "Data inválida"; return 'Data inválida';
} }
}; };
@@ -31,9 +28,7 @@ export const AtividadesRecentes: React.FC = () => {
<div className="space-y-4"> <div className="space-y-4">
{atividades.map((atividade: any) => ( {atividades.map((atividade: any) => (
<div key={atividade.id} className="flex items-start space-x-3"> <div key={atividade.id} className="flex items-start space-x-3">
<span className="text-2xl"> <span className="text-2xl">{atividade.tipo === 'PROCESSO' ? '⚖️' : '📋'}</span>
{atividade.tipo === "PROCESSO" ? "⚖️" : "📋"}
</span>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-gray-900 dark:text-white"> <p className="text-sm text-gray-900 dark:text-white">
{atividade.descricao} {atividade.descricao}
@@ -0,0 +1,65 @@
import React from 'react';
import { useBenchmarking } from '../../hooks/useBenchmarking';
import { CheckCircleIcon, XCircleIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
export const BenchmarkingEscritorio: React.FC = () => {
// Hook para buscar e calcular benchmarking
const { data: benchmarking, isLoading, error } = useBenchmarking();
if (isLoading) return <div>Carregando benchmarking...</div>;
if (error) return <div className="text-red-600">Erro ao buscar benchmarking</div>;
if (!benchmarking) return null;
// Cálculo da porcentagem de sucesso das causas
const totalCausas = benchmarking.procedentes + benchmarking.improcedentesOuSemMerito;
const sucessoPercent = totalCausas > 0 ? Math.round((benchmarking.procedentes / totalCausas) * 100) : 0;
return (
<div className="border rounded-xl p-6 shadow-lg bg-white dark:bg-gray-900 flex flex-col gap-6 max-w-md mx-auto">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-2 bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
<FolderOpenIcon className="h-7 w-7 text-blue-600" />
<div>
<div className="text-xs text-gray-500">Total de processos</div>
<div className="text-lg font-bold text-blue-700 dark:text-blue-300">{benchmarking.totalProcessos}</div>
</div>
</div>
<div className="flex items-center gap-2 bg-green-50 dark:bg-green-900 rounded-lg p-3">
<CheckCircleIcon className="h-7 w-7 text-green-600" />
<div>
<div className="text-xs text-gray-500">Processos ativos</div>
<div className="text-lg font-bold text-green-700 dark:text-green-300">{benchmarking.ativos}</div>
</div>
</div>
<div className="flex items-center gap-2 bg-green-50 dark:bg-green-900 rounded-lg p-3">
<CheckCircleIcon className="h-7 w-7 text-green-600" />
<div>
<div className="text-xs text-gray-500">Julgados procedentes</div>
<div className="text-lg font-bold text-green-700 dark:text-green-300">{benchmarking.procedentes}</div>
</div>
</div>
<div className="flex items-center gap-2 bg-red-50 dark:bg-red-900 rounded-lg p-3">
<XCircleIcon className="h-7 w-7 text-red-600" />
<div>
<div className="text-xs text-gray-500">Improcedentes/Sem mérito</div>
<div className="text-lg font-bold text-red-700 dark:text-red-300">{benchmarking.improcedentesOuSemMerito}</div>
</div>
</div>
<div className="flex items-center gap-2 bg-indigo-50 dark:bg-indigo-900 rounded-lg p-3">
<FolderOpenIcon className="h-7 w-7 text-indigo-600" />
<div>
<div className="text-xs text-gray-500">Ajuizados últimos 7 dias</div>
<div className="text-lg font-bold text-indigo-700 dark:text-indigo-300">{benchmarking.ajuizados7dias}</div>
</div>
</div>
<div className="flex items-center gap-2 bg-teal-50 dark:bg-teal-900 rounded-lg p-3">
<CheckCircleIcon className="h-7 w-7 text-teal-600" />
<div>
<div className="text-xs text-gray-500">% de sucesso das causas</div>
<div className="text-lg font-extrabold text-teal-700 dark:text-teal-300">{sucessoPercent}%</div>
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,117 @@
import React from 'react';
import { useCalendarioAtendimentos } from '../../hooks/useCalendarioAtendimentos';
import { CalendarIcon, UserIcon, PresentationChartBarIcon } from '@heroicons/react/24/outline';
export const CalendarioAtendimentos: React.FC = () => {
const { data: eventos = [], isLoading, error } = useCalendarioAtendimentos();
// Estado para mês e ano selecionados
const hoje = new Date();
const [mesSelecionado, setMesSelecionado] = React.useState(hoje.getMonth() + 1);
const [anoSelecionado, setAnoSelecionado] = React.useState(hoje.getFullYear());
const diasMes = Array.from({ length: new Date(anoSelecionado, mesSelecionado, 0).getDate() }, (_, i) => i + 1);
const mesStr = mesSelecionado.toString().padStart(2, '0');
const anoStr = anoSelecionado.toString();
// Atualiza data selecionada ao trocar mês/ano
const [dataSelecionada, setDataSelecionada] = React.useState(`${anoStr}-${mesStr}-${hoje.getDate().toString().padStart(2, '0')}`);
React.useEffect(() => {
setDataSelecionada(`${anoStr}-${mesStr}-${hoje.getDate().toString().padStart(2, '0')}`);
}, [mesSelecionado, anoSelecionado]);
// Separar eventos futuros/próximos e passados considerando ano/mês
const hojeStr = `${anoStr}-${mesStr}-${hoje.getDate().toString().padStart(2, '0')}`;
const eventosProximos = eventos
.filter(ev => ev.data >= hojeStr)
.sort((a, b) => a.data.localeCompare(b.data))
.slice(0, 10);
const eventosPassados = eventos
.filter(ev => ev.data < hojeStr)
.sort((a, b) => b.data.localeCompare(a.data))
.slice(0, 10);
const eventosDoDia = eventos.filter(e => e.data === dataSelecionada);
if (isLoading) return <div>Carregando calendário...</div>;
if (error) return <div className="text-red-600">Erro ao buscar eventos</div>;
return (
<div className="border rounded-xl p-4 shadow-lg bg-white dark:bg-gray-900 flex flex-col gap-6 max-w-4xl mx-auto">
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100 mb-2 flex items-center gap-2">
<CalendarIcon className="h-6 w-6 text-blue-600" /> Calendário de Atendimentos e Audiências
</h2>
<div className="flex items-center gap-4 mb-2">
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setMesSelecionado(m => m === 1 ? 12 : m - 1)}>&lt;</button>
<span className="font-semibold text-lg">{mesStr}/{anoStr}</span>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setMesSelecionado(m => m === 12 ? 1 : m + 1)}>&gt;</button>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setAnoSelecionado(a => a - 1)}>&lt; Ano</button>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setAnoSelecionado(a => a + 1)}>Ano &gt;</button>
</div>
<div className="grid grid-cols-7 gap-2 mb-4">
{diasMes.map((dia) => {
const dataStr = `${anoStr}-${mesStr}-${dia.toString().padStart(2, '0')}`;
const eventosDia = eventos.filter(e => e.data === dataStr);
const selecionado = dataStr === dataSelecionada;
return (
<button
key={dia}
type="button"
onClick={() => setDataSelecionada(dataStr)}
className={`h-12 w-full flex flex-col items-center justify-center border rounded-lg shadow-sm transition-all text-[11px] focus:outline-none ${selecionado ? 'ring-2 ring-blue-500 border-blue-500 bg-blue-100 dark:bg-blue-900' : eventosDia.length ? 'bg-blue-50 dark:bg-blue-900 border-blue-400' : 'bg-gray-50 dark:bg-gray-800'}`}
>
<span className="font-bold text-[11px] text-gray-700 dark:text-gray-200">{dia}</span>
{eventosDia.map((ev) => (
<span key={ev.id} className={`text-[10px] font-semibold flex items-center gap-1 whitespace-nowrap ${ev.tipo === 'Audiência' ? 'text-red-600' : 'text-blue-700'}`}>
{ev.tipo === 'Audiência' ? <PresentationChartBarIcon className="h-3 w-3" /> : <UserIcon className="h-3 w-3" />} {ev.tipo}
</span>
))}
</button>
);
})}
</div>
<div className="mb-6">
<h3 className="font-semibold mb-2 flex items-center gap-2"><CalendarIcon className="h-5 w-5 text-blue-600" /> Eventos do dia {dataSelecionada.split('-').reverse().join('/')}</h3>
<ul className="text-xs divide-y divide-gray-200 dark:divide-gray-700">
{eventosDoDia.length === 0 && <li className="text-gray-400">Não existem eventos no dia selecionado.</li>}
{eventosDoDia.map((ev) => (
<li key={ev.id} className="py-2 flex flex-wrap items-center gap-3">
{ev.tipo === 'Audiência' ? <PresentationChartBarIcon className="h-4 w-4 text-red-600" /> : <UserIcon className="h-4 w-4 text-blue-600" />}
<span className="px-2 py-1 rounded text-xs font-semibold bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 min-w-[80px] text-center">{ev.tipo}</span>
<span className="text-gray-600 dark:text-gray-300 break-words max-w-xs">{ev.descricao}</span>
</li>
))}
</ul>
</div>
<div className="grid grid-cols-2 gap-8">
<div>
<h3 className="font-semibold mb-2 flex items-center gap-2"><CalendarIcon className="h-5 w-5 text-blue-600" /> Próximos eventos</h3>
<ul className="text-xs divide-y divide-gray-200 dark:divide-gray-700">
{eventosProximos.length === 0 && <li className="text-gray-400">Nenhum evento futuro</li>}
{eventosProximos.map((ev) => (
<li key={ev.id} className="py-2 flex flex-wrap items-center gap-3">
{ev.tipo === 'Audiência' ? <PresentationChartBarIcon className="h-4 w-4 text-red-600" /> : <UserIcon className="h-4 w-4 text-blue-600" />}
<span className="font-bold min-w-[70px]">{ev.data}</span>
<span className="px-2 py-1 rounded text-xs font-semibold bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 min-w-[80px] text-center">{ev.tipo}</span>
<span className="text-gray-600 dark:text-gray-300 break-words max-w-xs">{ev.descricao}</span>
</li>
))}
</ul>
</div>
<div>
<h3 className="font-semibold mb-2 flex items-center gap-2"><CalendarIcon className="h-5 w-5 text-gray-500" /> Últimos eventos</h3>
<ul className="text-xs divide-y divide-gray-200 dark:divide-gray-700">
{eventosPassados.length === 0 && <li className="text-gray-400">Nenhum evento anterior</li>}
{eventosPassados.map((ev) => (
<li key={ev.id} className="py-2 flex flex-wrap items-center gap-3">
{ev.tipo === 'Audiência' ? <PresentationChartBarIcon className="h-4 w-4 text-red-600" /> : <UserIcon className="h-4 w-4 text-blue-600" />}
<span className="font-bold min-w-[70px]">{ev.data}</span>
<span className="px-2 py-1 rounded text-xs font-semibold bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 min-w-[80px] text-center">{ev.tipo}</span>
<span className="text-gray-600 dark:text-gray-300 break-words max-w-xs">{ev.descricao}</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
};
+25 -24
View File
@@ -1,5 +1,8 @@
import { ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/outline"; import React from 'react';
import React from "react"; import {
ArrowUpIcon,
ArrowDownIcon,
} from '@heroicons/react/24/outline';
interface CardProps { interface CardProps {
title: string; title: string;
@@ -7,9 +10,9 @@ interface CardProps {
trend?: number; trend?: number;
trendLabel?: string; trendLabel?: string;
icon?: string; icon?: string;
color?: "blue" | "green" | "red" | "yellow" | "purple"; color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple';
items?: Array<{ label: string; value: string | number }>; items?: Array<{ label: string; value: string | number }>;
type?: "default" | "list"; type?: 'default' | 'list';
} }
export const Card: React.FC<CardProps> = ({ export const Card: React.FC<CardProps> = ({
@@ -18,27 +21,27 @@ export const Card: React.FC<CardProps> = ({
trend, trend,
trendLabel, trendLabel,
icon, icon,
color = "blue", color = 'blue',
items, items,
type = "default", type = 'default'
}) => { }) => {
const colorClasses = { const colorClasses = {
blue: "text-blue-600", blue: 'text-blue-600',
green: "text-green-600", green: 'text-green-600',
red: "text-red-600", red: 'text-red-600',
yellow: "text-yellow-600", yellow: 'text-yellow-600',
purple: "text-purple-600", purple: 'text-purple-600'
}; };
const bgColorClasses = { const bgColorClasses = {
blue: "bg-blue-50", blue: 'bg-blue-50',
green: "bg-green-50", green: 'bg-green-50',
red: "bg-red-50", red: 'bg-red-50',
yellow: "bg-yellow-50", yellow: 'bg-yellow-50',
purple: "bg-purple-50", purple: 'bg-purple-50'
}; };
if (type === "list" && items) { if (type === 'list' && items) {
return ( return (
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-900 mb-4">{title}</h3> <h3 className="text-sm font-medium text-gray-900 mb-4">{title}</h3>
@@ -46,9 +49,7 @@ export const Card: React.FC<CardProps> = ({
{items.map((item, index) => ( {items.map((item, index) => (
<div key={index} className="flex justify-between items-center"> <div key={index} className="flex justify-between items-center">
<span className="text-sm text-gray-500">{item.label}</span> <span className="text-sm text-gray-500">{item.label}</span>
<span className="text-sm font-medium text-gray-900"> <span className="text-sm font-medium text-gray-900">{item.value}</span>
{item.value}
</span>
</div> </div>
))} ))}
</div> </div>
@@ -71,9 +72,7 @@ export const Card: React.FC<CardProps> = ({
{value} {value}
</p> </p>
{trend !== undefined && ( {trend !== undefined && (
<div <div className={`ml-2 flex items-center text-sm ${trend >= 0 ? 'text-green-600' : 'text-red-600'}`}>
className={`ml-2 flex items-center text-sm ${trend >= 0 ? "text-green-600" : "text-red-600"}`}
>
{trend >= 0 ? ( {trend >= 0 ? (
<ArrowUpIcon className="h-4 w-4" /> <ArrowUpIcon className="h-4 w-4" />
) : ( ) : (
@@ -81,7 +80,9 @@ export const Card: React.FC<CardProps> = ({
)} )}
<span className="ml-1">{Math.abs(trend)}%</span> <span className="ml-1">{Math.abs(trend)}%</span>
{trendLabel && ( {trendLabel && (
<span className="ml-1 text-gray-500">{trendLabel}</span> <span className="ml-1 text-gray-500">
{trendLabel}
</span>
)} )}
</div> </div>
)} )}
+21 -49
View File
@@ -1,58 +1,30 @@
import React from "react"; import React from 'react';
import { WorkflowTarefas } from './WorkflowTarefas';
import { useAuth } from "../../contexts/AuthContext"; import { CalendarioAtendimentos } from './CalendarioAtendimentos';
import { useDashboardStats } from "../../hooks/useDashboardStats"; import { BenchmarkingEscritorio } from './BenchmarkingEscritorio';
import { PainelFinanceiro } from './PainelFinanceiro';
import { AlertasPrazos } from "./AlertasPrazos";
import { AtividadesRecentes } from "./AtividadesRecentes";
import { AtendimentosStats } from "./stats/AtendimentosStats";
import { FinanceiroStats } from "./stats/FinanceiroStats";
import { KPIsFinanceiros } from "./stats/KPIsFinanceiros";
import { ProcessosStats } from "./stats/ProcessosStats";
import { TarefasStats } from "./stats/TarefasStats";
import { TaxaSucessoCard } from "./TaxaSucessoCard";
export const DashboardLayout: React.FC = () => { export const DashboardLayout: React.FC = () => {
const { user } = useAuth();
const {
processosStats,
tarefasStats,
atendimentosStats,
financeiroStats,
proximosPrazos,
atividades,
} = useDashboardStats();
const isAdminOrAdvogado = ["ADVOGADO_ADMIN", "ADVOGADO"].includes(
user?.perfil || "",
);
return ( return (
<div className="space-y-6 p-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 p-6">
{/* Stats Cards */} {/* Coluna 1: Workflow de Tarefas */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{processosStats.data && <ProcessosStats data={processosStats.data} />}
{tarefasStats.data && <TarefasStats data={tarefasStats.data} />}
{atendimentosStats.data && (
<AtendimentosStats data={atendimentosStats.data} />
)}
{isAdminOrAdvogado && financeiroStats.data && (
<FinanceiroStats data={financeiroStats.data} />
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Coluna da esquerda */}
<div className="lg:col-span-2 space-y-6">
<TaxaSucessoCard />
{isAdminOrAdvogado && <KPIsFinanceiros />}
</div>
{/* Coluna da direita */}
<div className="space-y-6"> <div className="space-y-6">
<AlertasPrazos /> <h2 className="text-xl font-bold">Workflow de Tarefas</h2>
<AtividadesRecentes /> <WorkflowTarefas />
</div> </div>
{/* Coluna 2: Calendário de Atendimentos, Compromissos e Diligências */}
<div className="space-y-6">
<h2 className="text-xl font-bold">Calendário</h2>
<CalendarioAtendimentos />
</div>
{/* Coluna 3: Benchmarking do Escritório */}
<div className="space-y-6">
<h2 className="text-xl font-bold">Benchmarking do Escritório</h2>
<BenchmarkingEscritorio />
<PainelFinanceiro />
</div> </div>
</div> </div>
); );
+9 -22
View File
@@ -1,11 +1,10 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import api from '../../services/api';
import api from "../../services/api";
export const KPIsAdvogados: React.FC = () => { export const KPIsAdvogados: React.FC = () => {
const { data: kpis } = useQuery("advogadosKpis", async () => { const { data: kpis } = useQuery('advogadosKpis', async () => {
const response = await api.get("/dashboard/advogados/kpis"); const response = await api.get('/dashboard/advogados/kpis');
return response.data; return response.data;
}); });
@@ -20,28 +19,16 @@ export const KPIsAdvogados: React.FC = () => {
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900"> <thead className="bg-gray-50 dark:bg-gray-900">
<tr> <tr>
<th <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Advogado Advogado
</th> </th>
<th <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Tarefas Concluídas Tarefas Concluídas
</th> </th>
<th <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Processos Concluídos Processos Concluídos
</th> </th>
<th <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Produtividade Produtividade
</th> </th>
</tr> </tr>
@@ -0,0 +1,83 @@
import React from 'react';
import { useFinanceiroPainel } from '../../hooks/useFinanceiroPainel';
import { CurrencyDollarIcon, ArrowDownCircleIcon, ArrowUpCircleIcon } from '@heroicons/react/24/solid';
export const PainelFinanceiro: React.FC = () => {
const hoje = new Date();
const [mes, setMes] = React.useState(hoje.getMonth() + 1);
const [ano, setAno] = React.useState(hoje.getFullYear());
const { data, isLoading, error } = useFinanceiroPainel(mes, ano);
const mesesNomes = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
return (
<div className="border rounded-xl p-6 shadow-lg bg-white dark:bg-gray-900 flex flex-col gap-6 max-w-4xl mx-auto mt-8">
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100 mb-2 flex items-center gap-2">
<CurrencyDollarIcon className="h-6 w-6 text-green-600" /> Painel Financeiro
</h2>
<div className="flex items-center gap-4 mb-2">
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setMes(m => m === 1 ? 12 : m - 1)}>&lt;</button>
<span className="font-semibold text-lg">{mesesNomes[mes - 1]}/{ano}</span>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setMes(m => m === 12 ? 1 : m + 1)}>&gt;</button>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setAno(a => a - 1)}>&lt; Ano</button>
<button type="button" className="px-2 py-1 rounded bg-gray-100 dark:bg-gray-800" onClick={() => setAno(a => a + 1)}>Ano &gt;</button>
</div>
{isLoading && <div>Carregando dados financeiros...</div>}
{error && <div className="text-red-600">Erro ao buscar dados financeiros</div>}
{data && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="flex flex-col items-center bg-green-50 dark:bg-green-900 rounded-lg p-4">
<CurrencyDollarIcon className="h-7 w-7 text-green-600 mb-1" />
<span className="text-xs text-gray-500">Honorários a receber</span>
<span className="text-lg font-bold text-green-700 dark:text-green-300">R$ {data.honorariosAReceber.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
</div>
<div className="flex flex-col items-center bg-blue-50 dark:bg-blue-900 rounded-lg p-4">
<ArrowUpCircleIcon className="h-7 w-7 text-blue-600 mb-1" />
<span className="text-xs text-gray-500">Honorários recebidos no mês</span>
<span className="text-lg font-bold text-blue-700 dark:text-blue-300">R$ {data.honorariosRecebidosMes.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
</div>
<div className="flex flex-col items-center bg-red-50 dark:bg-red-900 rounded-lg p-4">
<ArrowDownCircleIcon className="h-7 w-7 text-red-600 mb-1" />
<span className="text-xs text-gray-500">Custas a pagar</span>
<span className="text-lg font-bold text-red-700 dark:text-red-300">R$ {data.custasAPagar.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
</div>
<div className="flex flex-col items-center bg-teal-50 dark:bg-teal-900 rounded-lg p-4">
<ArrowUpCircleIcon className="h-7 w-7 text-teal-600 mb-1" />
<span className="text-xs text-gray-500">Custas pagas no mês</span>
<span className="text-lg font-bold text-teal-700 dark:text-teal-300">R$ {data.custasPagasMes.toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
</div>
</div>
)}
{data && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="font-semibold mb-2 text-green-700 dark:text-green-300">Honorários recebidos no mês</h3>
<ul className="text-xs divide-y divide-gray-200 dark:divide-gray-700">
{data.detalhesHonorarios.length === 0 && <li className="text-gray-400">Nenhum recebimento registrado neste mês.</li>}
{data.detalhesHonorarios.map((item, idx) => (
<li key={idx} className="py-2 flex flex-col">
<span className="font-bold">Contrato: {item.contrato?.processoId || 'N/A'}</span>
<span>Parcela: {item.parcela?.numeroParcela || 'N/A'} - Valor: R$ {Number(item.parcela?.valorPago || item.parcela?.valor || 0).toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
<span className="text-gray-500">Pago em: {item.parcela?.dataPagamento ? new Date(item.parcela.dataPagamento).toLocaleDateString('pt-br') : '-'}</span>
</li>
))}
</ul>
</div>
<div>
<h3 className="font-semibold mb-2 text-red-700 dark:text-red-300">Custas pagas no mês</h3>
<ul className="text-xs divide-y divide-gray-200 dark:divide-gray-700">
{data.detalhesCustas.length === 0 && <li className="text-gray-400">Nenhuma custa paga neste mês.</li>}
{data.detalhesCustas.map((c, idx) => (
<li key={idx} className="py-2 flex flex-col">
<span className="font-bold">Processo: {c.processoId || 'N/A'}</span>
<span>Tipo: {c.tipo || 'N/A'} - Valor: R$ {Number(c.valor || 0).toLocaleString('pt-br', { minimumFractionDigits: 2 })}</span>
<span className="text-gray-500">Pago em: {c.dataPagamento ? new Date(c.dataPagamento).toLocaleDateString('pt-br') : '-'}</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
);
};
+13 -23
View File
@@ -1,46 +1,36 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import api from '../../services/api';
import api from "../../services/api";
export const TaxaSucessoCard: React.FC = () => { export const TaxaSucessoCard: React.FC = () => {
const { data: stats } = useQuery("taxa-sucesso", async () => { const { data: stats } = useQuery('taxa-sucesso', async () => {
const response = await api.get("/dashboard/processos/taxa-sucesso"); const response = await api.get('/dashboard/processos/taxa-sucesso');
return response.data; return response.data;
}); });
if (!stats) return null; if (!stats) return null;
const estatisticas = stats || []; const estatisticas = stats || [];
const total = estatisticas.reduce( const total = estatisticas.reduce((acc: number, curr: any) => acc + (curr._count || 0), 0);
(acc: number, curr: any) => acc + (curr._count || 0),
0,
);
return ( return (
<div className="bg-white rounded-lg shadow p-6"> <div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Taxa de Sucesso</h3> <h3 className="text-lg font-semibold mb-4">Taxa de Sucesso</h3>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-3xl font-bold text-green-600"> <div className="text-3xl font-bold text-green-600">
{estatisticas.find((s: any) => s.resultado === "PROCEDENTE")?._count {estatisticas.find((s: any) => s.resultado === 'PROCEDENTE')?._count
? Math.round( ? Math.round((estatisticas.find((s: any) => s.resultado === 'PROCEDENTE')._count / total) * 100)
(estatisticas.find((s: any) => s.resultado === "PROCEDENTE") : 0}%
._count / </div>
total) * <div className="text-sm text-gray-500">
100, Total julgados: {total}
)
: 0}
%
</div> </div>
<div className="text-sm text-gray-500">Total julgados: {total}</div>
</div> </div>
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
{estatisticas.map((item: any) => ( {estatisticas.map((item: any) => (
<div key={item.resultado} className="flex justify-between"> <div key={item.resultado} className="flex justify-between">
<span className="text-sm text-gray-600">{item.resultado}</span> <span className="text-sm text-gray-600">{item.resultado}</span>
<span className="text-sm font-medium"> <span className="text-sm font-medium">{Math.round((item._count / total) * 100)}%</span>
{Math.round((item._count / total) * 100)}%
</span>
</div> </div>
))} ))}
</div> </div>
@@ -0,0 +1,106 @@
import React from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { useTarefas } from '../../hooks/useTarefas';
import { Tarefa } from '../../types/entities';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
export const WorkflowTarefas: React.FC = () => {
const { user } = useAuth();
const { tarefas, isLoading, deleteTarefa } = useTarefas();
const canDelete = user && ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user.perfil);
const handleEdit = (tarefa: Tarefa) => {
// Implemente a lógica de edição conforme necessário
alert(`Editar tarefa: ${tarefa.titulo}`);
};
const handleDelete = async (id: string) => {
if (!window.confirm('Tem certeza que deseja excluir esta tarefa?')) return;
try {
await deleteTarefa.mutateAsync(id);
} catch (error) {
console.error('Erro ao excluir tarefa:', error);
alert('Erro ao excluir tarefa');
}
};
if (isLoading) {
return (
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-4 text-center text-gray-500 dark:text-gray-400">
Carregando tarefas...
</div>
);
}
if (!tarefas || tarefas.length === 0) {
return (
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-4 text-center text-gray-500 dark:text-gray-400">
Nenhuma tarefa encontrada.
</div>
);
}
// Exibir apenas tarefas que não estão concluídas ou canceladas
const tarefasAtivas = tarefas.filter((t: Tarefa) => t.status !== 'CONCLUIDA' && t.status !== 'CANCELADA');
return (
<div className="flex flex-col gap-4">
{tarefasAtivas.map((tarefa: Tarefa) => (
<div key={tarefa.id} className="border rounded-lg p-4 shadow flex flex-col gap-2 bg-white dark:bg-gray-800">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-2">
<div className="flex items-center gap-3">
<span className="font-medium text-lg text-gray-900 dark:text-white">{tarefa.titulo}</span>
{/* Exibe o grau de prioridade com cor */}
<span className={`px-2 py-1 rounded text-xs font-semibold border ${
tarefa.prioridade === 'URGENTE' ? 'bg-red-100 text-red-700 border-red-300' :
tarefa.prioridade === 'ALTA' ? 'bg-orange-100 text-orange-700 border-orange-300' :
tarefa.prioridade === 'MEDIA' ? 'bg-yellow-100 text-yellow-700 border-yellow-300' :
'bg-green-100 text-green-700 border-green-300'
}`}>
{tarefa.prioridade === 'URGENTE' ? 'Urgente' : tarefa.prioridade === 'ALTA' ? 'Alta' : tarefa.prioridade === 'MEDIA' ? 'Média' : 'Baixa'}
</span>
</div>
<div className="flex items-center gap-3">
{/* Status da tarefa */}
<span className={`px-2 py-1 rounded text-xs font-semibold border ${
tarefa.status === 'CONCLUIDA' ? 'bg-green-100 text-green-700 border-green-300' :
tarefa.status === 'EM_ANDAMENTO' ? 'bg-blue-100 text-blue-700 border-blue-300' :
tarefa.status === 'PENDENTE' ? 'bg-gray-100 text-gray-700 border-gray-300' :
'bg-red-100 text-red-700 border-red-300'
}`}>
{tarefa.status.replace('_', ' ')}
</span>
<button
onClick={() => handleEdit(tarefa)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
title="Editar"
>
<PencilIcon className="h-5 w-5" />
</button>
{canDelete && (
<button
onClick={() => handleDelete(tarefa.id)}
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
)}
</div>
</div>
{/* Listagem das subtarefas vinculadas */}
{tarefa.subtarefas && tarefa.subtarefas.length > 0 && (
<ul className="mt-2 ml-2 list-disc text-sm text-gray-700 dark:text-gray-300">
{tarefa.subtarefas.map((sub) => (
<li key={sub.id} className={sub.concluida ? 'line-through text-gray-400' : ''}>
{sub.titulo} {sub.concluida ? '(Concluída)' : ''}
</li>
))}
</ul>
)}
</div>
))}
</div>
);
};
+12 -16
View File
@@ -1,24 +1,20 @@
import { format } from "date-fns"; import React from 'react';
import { ptBR } from "date-fns/locale"; import { useQuery } from 'react-query';
import React from "react"; import { format } from 'date-fns';
import { useQuery } from "react-query"; import { ptBR } from 'date-fns/locale';
import api from '../../../services/api';
import api from "../../../services/api";
export const MapaCalor: React.FC = () => { export const MapaCalor: React.FC = () => {
const { data: atividadesDiarias } = useQuery( const { data: atividadesDiarias } = useQuery('atividadesDiarias', async () => {
"atividadesDiarias", const response = await api.get('/dashboard/atividades/diarias');
async () => {
const response = await api.get("/dashboard/atividades/diarias");
return response.data; return response.data;
}, });
);
const intensidadeCor = (quantidade: number) => { const intensidadeCor = (quantidade: number) => {
if (quantidade === 0) return "bg-gray-100 dark:bg-gray-800"; if (quantidade === 0) return 'bg-gray-100 dark:bg-gray-800';
if (quantidade <= 3) return "bg-blue-100 dark:bg-blue-900"; if (quantidade <= 3) return 'bg-blue-100 dark:bg-blue-900';
if (quantidade <= 6) return "bg-blue-300 dark:bg-blue-700"; if (quantidade <= 6) return 'bg-blue-300 dark:bg-blue-700';
return "bg-blue-500 dark:bg-blue-500"; return 'bg-blue-500 dark:bg-blue-500';
}; };
return ( return (
@@ -1,28 +1,19 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
BarChart, import api from '../../../services/api';
Bar, import { useAuth } from '../../../contexts/AuthContext';
XAxis, import { formatCurrency } from '../../../utils/formatters';
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { useAuth } from "../../../contexts/AuthContext";
import api from "../../../services/api";
import { formatCurrency } from "../../../utils/formatters";
export const PrevisaoReceitasChart: React.FC = () => { export const PrevisaoReceitasChart: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
if (!["ADVOGADO_ADMIN", "ADVOGADO"].includes(user?.perfil || "")) { if (!['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || '')) {
return null; return null;
} }
const { data: previsao } = useQuery("previsaoReceitas", async () => { const { data: previsao } = useQuery('previsaoReceitas', async () => {
const response = await api.get("/dashboard/financeiro/previsao"); const response = await api.get('/dashboard/financeiro/previsao');
return response.data; return response.data;
}); });
@@ -1,15 +1,12 @@
import React from "react"; import React from 'react';
import { useAuth } from '../../../contexts/AuthContext';
import { useAuth } from "../../../contexts/AuthContext"; import { ProcessosEvolucao } from './ProcessosEvolucao';
import { TaxaSucessoChart } from './TaxaSucessoChart';
import { PrevisaoReceitasChart } from "./PrevisaoReceitasChart"; import { PrevisaoReceitasChart } from './PrevisaoReceitasChart';
import { ProcessosEvolucao } from "./ProcessosEvolucao";
import { TaxaSucessoChart } from "./TaxaSucessoChart";
export const ProcessosCharts: React.FC = () => { export const ProcessosCharts: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
const isAdminOrAdvogado = const isAdminOrAdvogado = user?.perfil === 'ADVOGADO_ADMIN' || user?.perfil === 'ADVOGADO';
user?.perfil === "ADVOGADO_ADMIN" || user?.perfil === "ADVOGADO";
return ( return (
<> <>
@@ -1,20 +1,11 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
LineChart, import api from '../../../services/api';
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import api from "../../../services/api";
export const ProcessosEvolucao: React.FC = () => { export const ProcessosEvolucao: React.FC = () => {
const { data: evolucao } = useQuery("processosEvolucao", async () => { const { data: evolucao } = useQuery('processosEvolucao', async () => {
const response = await api.get("/dashboard/processos/evolucao"); const response = await api.get('/dashboard/processos/evolucao');
return response.data; return response.data;
}); });
@@ -30,18 +21,8 @@ export const ProcessosEvolucao: React.FC = () => {
<XAxis dataKey="mes" /> <XAxis dataKey="mes" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Line <Line type="monotone" dataKey="novos" stroke="#3B82F6" name="Novos" />
type="monotone" <Line type="monotone" dataKey="concluidos" stroke="#10B981" name="Concluídos" />
dataKey="novos"
stroke="#3B82F6"
name="Novos"
/>
<Line
type="monotone"
dataKey="concluidos"
stroke="#10B981"
name="Concluídos"
/>
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -1,23 +1,15 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
PieChart, import api from '../../../services/api';
Pie,
Cell,
ResponsiveContainer,
Legend,
Tooltip,
} from "recharts";
import api from "../../../services/api";
export const TaxaSucessoChart: React.FC = () => { export const TaxaSucessoChart: React.FC = () => {
const { data: stats } = useQuery("taxaSucesso", async () => { const { data: stats } = useQuery('taxaSucesso', async () => {
const response = await api.get("/dashboard/processos/taxa-sucesso"); const response = await api.get('/dashboard/processos/taxa-sucesso');
return response.data; return response.data;
}); });
const COLORS = ["#10B981", "#EF4444", "#F59E0B"]; const COLORS = ['#10B981', '#EF4444', '#F59E0B'];
return ( return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
@@ -37,10 +29,7 @@ export const TaxaSucessoChart: React.FC = () => {
label label
> >
{stats?.map((entry: any, index: number) => ( {stats?.map((entry: any, index: number) => (
<Cell <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))} ))}
</Pie> </Pie>
<Tooltip /> <Tooltip />
@@ -1,12 +1,11 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from "../../../services/api"; import api from '../../../services/api';
import { Card } from "../Card";
export const AtendimentosStats: React.FC = () => { export const AtendimentosStats: React.FC = () => {
const { data: stats } = useQuery("atendimentosStats", async () => { const { data: stats } = useQuery('atendimentosStats', async () => {
const response = await api.get("/dashboard/atendimentos/stats"); const response = await api.get('/dashboard/atendimentos/stats');
return response.data; return response.data;
}); });
@@ -1,13 +1,12 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from "../../../services/api"; import api from '../../../services/api';
import { formatCurrency } from "../../../utils/formatters"; import { formatCurrency } from '../../../utils/formatters';
import { Card } from "../Card";
export const FinanceiroStats: React.FC = () => { export const FinanceiroStats: React.FC = () => {
const { data: stats } = useQuery("financeiroStats", async () => { const { data: stats } = useQuery('financeiroStats', async () => {
const response = await api.get("/dashboard/financeiro/stats"); const response = await api.get('/dashboard/financeiro/stats');
return response.data; return response.data;
}); });
@@ -1,20 +1,19 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { useAuth } from '../../../contexts/AuthContext';
import { useAuth } from "../../../contexts/AuthContext"; import { Card } from '../../dashboard/Card';
import api from "../../../services/api"; import api from '../../../services/api';
import { formatCurrency } from "../../../utils/formatters"; import { formatCurrency } from '../../../utils/formatters';
import { Card } from "../../dashboard/Card";
export const KPIsFinanceiros: React.FC = () => { export const KPIsFinanceiros: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
if (!["ADVOGADO_ADMIN", "ADVOGADO"].includes(user?.perfil || "")) { if (!['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || '')) {
return null; return null;
} }
const { data: stats } = useQuery("financeiroKPIs", async () => { const { data: stats } = useQuery('financeiroKPIs', async () => {
const response = await api.get("/dashboard/financeiro/stats"); const response = await api.get('/dashboard/financeiro/stats');
return response.data; return response.data;
}); });
@@ -1,12 +1,11 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import api from '../../../services/api';
import api from "../../../services/api"; import { Card } from '../Card';
import { Card } from "../Card";
export const ProcessosStats: React.FC = () => { export const ProcessosStats: React.FC = () => {
const { data: stats } = useQuery("processosStats", async () => { const { data: stats } = useQuery('processosStats', async () => {
const response = await api.get("/dashboard/processos/stats"); const response = await api.get('/dashboard/processos/stats');
return response.data; return response.data;
}); });
@@ -18,8 +17,16 @@ export const ProcessosStats: React.FC = () => {
trend={stats?.crescimento || 0} trend={stats?.crescimento || 0}
icon="DocumentText" icon="DocumentText"
/> />
<Card title="Por Tribunal" items={stats?.porTribunal || []} type="list" /> <Card
<Card title="Por Status" items={stats?.porStatus || []} type="list" /> title="Por Tribunal"
items={stats?.porTribunal || []}
type="list"
/>
<Card
title="Por Status"
items={stats?.porStatus || []}
type="list"
/>
</div> </div>
); );
}; };
@@ -1,23 +1,22 @@
import React from "react"; import React from 'react';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { Card } from '../Card';
import api from "../../../services/api"; import api from '../../../services/api';
import { Card } from "../Card";
export const TarefasStats: React.FC = () => { export const TarefasStats: React.FC = () => {
const { data: stats } = useQuery("tarefasStats", async () => { const { data: stats } = useQuery('tarefasStats', async () => {
try { try {
const response = await api.get("/dashboard/tarefas/stats"); const response = await api.get('/dashboard/tarefas/stats');
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error("Erro ao buscar estatísticas:", error); console.error('Erro ao buscar estatísticas:', error);
return { return {
pendentes: 0, pendentes: 0,
atrasadas: 0, atrasadas: 0,
concluidas: 0, concluidas: 0,
total: 0, total: 0,
taxaConclusao: 0, taxaConclusao: 0,
porStatus: { PENDENTE: 0 }, porStatus: { PENDENTE: 0 }
}; };
} }
}); });
@@ -28,7 +27,7 @@ export const TarefasStats: React.FC = () => {
concluidas: 0, concluidas: 0,
total: 0, total: 0,
taxaConclusao: 0, taxaConclusao: 0,
porStatus: { PENDENTE: 0 }, porStatus: { PENDENTE: 0 }
}; };
const safeStats = stats || defaultStats; const safeStats = stats || defaultStats;
@@ -1,11 +1,10 @@
import { Dialog } from "@headlessui/react"; import { useState, useEffect } from 'react';
import { DocumentArrowUpIcon } from "@heroicons/react/24/outline"; import { api } from '../../lib/api';
import { Editor } from "@tinymce/tinymce-react"; import { toast } from 'react-toastify';
import { useState, useEffect } from "react"; import { Dialog } from '@headlessui/react';
import { toast } from "react-toastify"; import { DocumentArrowUpIcon } from '@heroicons/react/24/outline';
import { Editor } from '@tinymce/tinymce-react';
import { api } from "../../lib/api"; import { DOCUMENT_VARIABLES } from '../../utils/documentVariables';
import { DOCUMENT_VARIABLES } from "../../utils/documentVariables";
// Definir interface para o estado // Definir interface para o estado
interface ConfigState { interface ConfigState {
@@ -25,18 +24,18 @@ interface ConfigState {
export function ConfiguracaoDocumento() { export function ConfiguracaoDocumento() {
const [config, setConfig] = useState<ConfigState>({ const [config, setConfig] = useState<ConfigState>({
timbradoUrl: "", timbradoUrl: '',
timbradoMarginTop: 100, timbradoMarginTop: 100,
timbradoMarginBottom: 50, timbradoMarginBottom: 50,
timbradoMarginLeft: 50, timbradoMarginLeft: 50,
timbradoMarginRight: 50, timbradoMarginRight: 50,
templatePadrao: "", templatePadrao: '',
margensTemplate: { margensTemplate: {
top: 35, top: 35,
bottom: 29, bottom: 29,
left: 25, left: 25,
right: 25, right: 25
}, }
}); });
const [isPreviewOpen, setPreviewOpen] = useState(false); const [isPreviewOpen, setPreviewOpen] = useState(false);
@@ -48,27 +47,27 @@ export function ConfiguracaoDocumento() {
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const response = await api.get("/documentos/configuracoes"); const response = await api.get('/documentos/configuracoes');
console.log("Configurações carregadas:", response.data); // Debug console.log('Configurações carregadas:', response.data); // Debug
if (response.data) { if (response.data) {
setConfig({ setConfig({
timbradoUrl: response.data.timbradoUrl || "", timbradoUrl: response.data.timbradoUrl || '',
timbradoMarginTop: response.data.timbradoMarginTop || 100, timbradoMarginTop: response.data.timbradoMarginTop || 100,
timbradoMarginBottom: response.data.timbradoMarginBottom || 50, timbradoMarginBottom: response.data.timbradoMarginBottom || 50,
timbradoMarginLeft: response.data.timbradoMarginLeft || 50, timbradoMarginLeft: response.data.timbradoMarginLeft || 50,
timbradoMarginRight: response.data.timbradoMarginRight || 50, timbradoMarginRight: response.data.timbradoMarginRight || 50,
templatePadrao: response.data.templatePadrao || "", templatePadrao: response.data.templatePadrao || '',
margensTemplate: { margensTemplate: {
top: response.data.margensTemplate?.top || 35, top: response.data.margensTemplate?.top || 35,
bottom: response.data.margensTemplate?.bottom || 29, bottom: response.data.margensTemplate?.bottom || 29,
left: response.data.margensTemplate?.left || 25, left: response.data.margensTemplate?.left || 25,
right: response.data.margensTemplate?.right || 25, right: response.data.margensTemplate?.right || 25
}, }
}); });
} }
} catch (error) { } catch (error) {
console.error("Erro ao carregar configurações:", error); console.error('Erro ao carregar configurações:', error);
toast.error("Erro ao carregar configurações"); toast.error('Erro ao carregar configurações');
} }
}; };
@@ -77,57 +76,53 @@ export function ConfiguracaoDocumento() {
if (!file) return; if (!file) return;
// Atualizado para aceitar PDF // Atualizado para aceitar PDF
if ( if (!['image/png', 'image/svg+xml', 'application/pdf'].includes(file.type)) {
!["image/png", "image/svg+xml", "application/pdf"].includes(file.type) toast.error('Por favor, use apenas arquivos PDF, PNG ou SVG');
) {
toast.error("Por favor, use apenas arquivos PDF, PNG ou SVG");
return; return;
} }
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) { // Aumentado para 5MB para acomodar PDFs
// Aumentado para 5MB para acomodar PDFs toast.error('O arquivo deve ter no máximo 5MB');
toast.error("O arquivo deve ter no máximo 5MB");
return; return;
} }
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("timbrado", file); formData.append('timbrado', file);
const response = await api.post("/documentos/upload-timbrado", formData); const response = await api.post('/documentos/upload-timbrado', formData);
setConfig((prev) => ({ ...prev, timbradoUrl: response.data.url })); setConfig(prev => ({ ...prev, timbradoUrl: response.data.url }));
toast.success("Timbrado atualizado com sucesso"); toast.success('Timbrado atualizado com sucesso');
} catch (error) { } catch (error) {
toast.error("Erro ao fazer upload do timbrado"); toast.error('Erro ao fazer upload do timbrado');
} }
}; };
const handleSave = async () => { const handleSave = async () => {
try { try {
setIsSaving(true); setIsSaving(true);
await api.post("/documentos/configuracoes", config); await api.post('/documentos/configuracoes', config);
toast.success("Configurações salvas com sucesso"); toast.success('Configurações salvas com sucesso');
} catch (error) { } catch (error) {
toast.error("Erro ao salvar configurações"); toast.error('Erro ao salvar configurações');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
const handleEditorSetup = (editor: any) => { const handleEditorSetup = (editor: any) => {
editor.ui.registry.addMenuButton("variables", { editor.ui.registry.addMenuButton('variables', {
text: "Variáveis", text: 'Variáveis',
fetch: (callback: Function) => { fetch: (callback: Function) => {
const items = Object.entries(DOCUMENT_VARIABLES).flatMap( const items = Object.entries(DOCUMENT_VARIABLES).flatMap(([category, vars]) =>
([category, vars]) =>
Object.entries(vars).map(([name, value]) => ({ Object.entries(vars).map(([name, value]) => ({
type: "menuitem", type: 'menuitem',
text: `${category} - ${name.replace(/_/g, " ")}`, text: `${category} - ${name.replace(/_/g, ' ')}`,
onAction: () => editor.insertContent(value as string), onAction: () => editor.insertContent(value as string)
})), }))
); );
callback(items); callback(items);
}, }
}); });
}; };
@@ -136,14 +131,12 @@ export function ConfiguracaoDocumento() {
height: 400, height: 400,
menubar: true, menubar: true,
plugins: [ plugins: [
"code", 'code', 'preview' // Removido 'template'
"preview", // Removido 'template'
], ],
toolbar: toolbar: 'variables | undo redo | formatselect | ' +
"variables | undo redo | formatselect | " + 'bold italic | alignleft aligncenter alignright | ' +
"bold italic | alignleft aligncenter alignright | " + 'code preview',
"code preview", setup: handleEditorSetup
setup: handleEditorSetup,
}; };
return ( return (
@@ -186,12 +179,7 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginTop} value={config.timbradoMarginTop}
onChange={(e) => onChange={e => setConfig(prev => ({ ...prev, timbradoMarginTop: Number(e.target.value) }))}
setConfig((prev) => ({
...prev,
timbradoMarginTop: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -200,12 +188,7 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginBottom} value={config.timbradoMarginBottom}
onChange={(e) => onChange={e => setConfig(prev => ({ ...prev, timbradoMarginBottom: Number(e.target.value) }))}
setConfig((prev) => ({
...prev,
timbradoMarginBottom: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -214,12 +197,7 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginLeft} value={config.timbradoMarginLeft}
onChange={(e) => onChange={e => setConfig(prev => ({ ...prev, timbradoMarginLeft: Number(e.target.value) }))}
setConfig((prev) => ({
...prev,
timbradoMarginLeft: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -228,12 +206,7 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginRight} value={config.timbradoMarginRight}
onChange={(e) => onChange={e => setConfig(prev => ({ ...prev, timbradoMarginRight: Number(e.target.value) }))}
setConfig((prev) => ({
...prev,
timbradoMarginRight: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -245,9 +218,7 @@ export function ConfiguracaoDocumento() {
<Editor <Editor
apiKey={import.meta.env.VITE_TINYMCE_API_KEY} apiKey={import.meta.env.VITE_TINYMCE_API_KEY}
value={config.templatePadrao} value={config.templatePadrao}
onEditorChange={(content) => onEditorChange={(content) => setConfig(prev => ({ ...prev, templatePadrao: content }))}
setConfig((prev) => ({ ...prev, templatePadrao: content }))
}
init={editorConfig} init={editorConfig}
/> />
<div className="mt-2 p-4 bg-gray-50 rounded border"> <div className="mt-2 p-4 bg-gray-50 rounded border">
@@ -255,9 +226,7 @@ export function ConfiguracaoDocumento() {
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Object.entries(DOCUMENT_VARIABLES).map(([category, vars]) => ( {Object.entries(DOCUMENT_VARIABLES).map(([category, vars]) => (
<div key={category}> <div key={category}>
<h5 className="font-medium text-sm text-gray-700"> <h5 className="font-medium text-sm text-gray-700">{category}</h5>
{category}
</h5>
<ul className="mt-1 space-y-1"> <ul className="mt-1 space-y-1">
{Object.entries(vars).map(([name, value]) => ( {Object.entries(vars).map(([name, value]) => (
<li key={name} className="text-xs text-gray-600"> <li key={name} className="text-xs text-gray-600">
@@ -278,15 +247,13 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.top} value={config.margensTemplate.top}
onChange={(e) => onChange={e => setConfig(prev => ({
setConfig((prev) => ({
...prev, ...prev,
margensTemplate: { margensTemplate: {
...prev.margensTemplate, ...prev.margensTemplate,
top: Number(e.target.value), top: Number(e.target.value)
},
}))
} }
}))}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -295,15 +262,13 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.bottom} value={config.margensTemplate.bottom}
onChange={(e) => onChange={e => setConfig(prev => ({
setConfig((prev) => ({
...prev, ...prev,
margensTemplate: { margensTemplate: {
...prev.margensTemplate, ...prev.margensTemplate,
bottom: Number(e.target.value), bottom: Number(e.target.value)
},
}))
} }
}))}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -312,15 +277,13 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.left} value={config.margensTemplate.left}
onChange={(e) => onChange={e => setConfig(prev => ({
setConfig((prev) => ({
...prev, ...prev,
margensTemplate: { margensTemplate: {
...prev.margensTemplate, ...prev.margensTemplate,
left: Number(e.target.value), left: Number(e.target.value)
},
}))
} }
}))}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -329,15 +292,13 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.right} value={config.margensTemplate.right}
onChange={(e) => onChange={e => setConfig(prev => ({
setConfig((prev) => ({
...prev, ...prev,
margensTemplate: { margensTemplate: {
...prev.margensTemplate, ...prev.margensTemplate,
right: Number(e.target.value), right: Number(e.target.value)
},
}))
} }
}))}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -348,11 +309,11 @@ export function ConfiguracaoDocumento() {
disabled={isSaving} disabled={isSaving}
className={`px-4 py-2 ${ className={`px-4 py-2 ${
isSaving isSaving
? "bg-gray-400 cursor-not-allowed" ? 'bg-gray-400 cursor-not-allowed'
: "bg-blue-600 hover:bg-blue-700" : 'bg-blue-600 hover:bg-blue-700'
} text-white rounded`} } text-white rounded`}
> >
{isSaving ? "Salvando..." : "Salvar Configurações"} {isSaving ? 'Salvando...' : 'Salvar Configurações'}
</button> </button>
</div> </div>
+75 -107
View File
@@ -1,18 +1,11 @@
import { XMarkIcon } from "@heroicons/react/24/outline"; import { useState, useEffect } from 'react';
import { Save as SaveIcon } from "@mui/icons-material"; import { Editor } from '@tinymce/tinymce-react';
import { import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
Button, import { Save as SaveIcon } from '@mui/icons-material';
Dialog, import { XMarkIcon } from '@heroicons/react/24/outline';
DialogTitle, import { toast } from 'react-toastify'; // Adicionar esta importação
DialogContent, import { api } from '../../lib/api'; // Adicionar esta importação
DialogActions, import { DocumentoService } from '../../services/DocumentoService'; // Adicionar esta importação
} from "@mui/material";
import { Editor } from "@tinymce/tinymce-react";
import { useState, useEffect } from "react";
import { toast } from "react-toastify"; // Adicionar esta importação
import { api } from "../../lib/api"; // Adicionar esta importação
import { DocumentoService } from "../../services/DocumentoService"; // Adicionar esta importação
interface DocumentoEditorProps { interface DocumentoEditorProps {
isOpen: boolean; isOpen: boolean;
@@ -34,7 +27,7 @@ interface TemplateVariables {
const preencherTemplate = (template: string, variables: TemplateVariables) => { const preencherTemplate = (template: string, variables: TemplateVariables) => {
let result = template; let result = template;
Object.entries(variables).forEach(([key, value]) => { Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`{{${key}}}`, "g"); const regex = new RegExp(`{{${key}}}`, 'g');
result = result.replace(regex, value); result = result.replace(regex, value);
}); });
return result; return result;
@@ -47,11 +40,11 @@ export function DocumentoEditor({
initialData, initialData,
processoId, processoId,
atendimentoId, atendimentoId,
documento, // Recuperar a prop documento // Recuperar a prop
}: DocumentoEditorProps) { }: DocumentoEditorProps) {
const [content, setContent] = useState(""); const [content, setContent] = useState('');
const [nome, setNome] = useState(""); const [nome, setNome] = useState('');
const [tipo, setTipo] = useState("OUTROS"); const [tipo, setTipo] = useState('OUTROS');
const [selectedTemplate, setSelectedTemplate] = useState(null); const [selectedTemplate, setSelectedTemplate] = useState(null);
const [selectedAtendimento, setSelectedAtendimento] = useState(null); const [selectedAtendimento, setSelectedAtendimento] = useState(null);
const [atendimentos, setAtendimentos] = useState([]); const [atendimentos, setAtendimentos] = useState([]);
@@ -61,9 +54,9 @@ export function DocumentoEditor({
useEffect(() => { useEffect(() => {
if (initialData) { if (initialData) {
setContent(initialData.conteudo || ""); setContent(initialData.conteudo || '');
setNome(initialData.nome || ""); setNome(initialData.nome || '');
setTipo(initialData.tipo || "OUTROS"); setTipo(initialData.tipo || 'OUTROS');
} }
}, [initialData]); }, [initialData]);
@@ -71,13 +64,13 @@ export function DocumentoEditor({
const loadData = async () => { const loadData = async () => {
try { try {
const [atendimentosRes, templatesRes] = await Promise.all([ const [atendimentosRes, templatesRes] = await Promise.all([
api.get("/atendimentos"), api.get('/atendimentos'),
api.get("/documentos-modelos"), api.get('/documentos-modelos')
]); ]);
setAtendimentos(atendimentosRes.data); setAtendimentos(atendimentosRes.data);
setTemplates(templatesRes.data); setTemplates(templatesRes.data);
} catch (error) { } catch (error) {
toast.error("Erro ao carregar dados"); toast.error('Erro ao carregar dados');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -87,38 +80,36 @@ export function DocumentoEditor({
useEffect(() => { useEffect(() => {
if (documento) { if (documento) {
setContent(documento.conteudo || ""); setContent(documento.conteudo || '');
setNome(documento.nome || ""); setNome(documento.nome || '');
setTipo(documento.tipo || "OUTROS"); setTipo(documento.tipo || 'OUTROS');
} }
}, [documento]); }, [documento]);
const handleTemplateSelect = async (templateId) => { const handleTemplateSelect = async (templateId) => {
try { try {
const template = templates.find((t) => t.id === templateId); const template = templates.find(t => t.id === templateId);
if (!template) return; if (!template) return;
const atendimento = atendimentos.find( const atendimento = atendimentos.find(a => a.id === selectedAtendimento);
(a) => a.id === selectedAtendimento,
);
if (!atendimento) return; if (!atendimento) return;
// Buscar dados do atendimento com as relações necessárias // Buscar dados do atendimento com as relações necessárias
const [atendimentoDetalhado, usuarioRes, configRes] = await Promise.all([ const [atendimentoDetalhado, usuarioRes, configRes] = await Promise.all([
api.get(`/atendimentos/${atendimento.id}`), api.get(`/atendimentos/${atendimento.id}`),
api.get("/auth/me"), api.get('/auth/me'),
api.get("/documentos/configuracoes"), api.get('/documentos/configuracoes')
]); ]);
// Formatar endereço completo e outros dados do cliente // Formatar endereço completo e outros dados do cliente
const cliente = atendimentoDetalhado.data.cliente; const cliente = atendimentoDetalhado.data.cliente;
const enderecoCompleto = `${cliente.endereco}, ${cliente.numero}${cliente.complemento ? `, ${cliente.complemento}` : ""}, ${cliente.bairro}, ${cliente.cidade}/${cliente.estado}, CEP: ${cliente.cep}`; const enderecoCompleto = `${cliente.endereco}, ${cliente.numero}${cliente.complemento ? `, ${cliente.complemento}` : ''}, ${cliente.bairro}, ${cliente.cidade}/${cliente.estado}, CEP: ${cliente.cep}`;
// Formatar data extenso corretamente // Formatar data extenso corretamente
const formatadorData = new Intl.DateTimeFormat("pt-BR", { const formatadorData = new Intl.DateTimeFormat('pt-BR', {
day: "numeric", day: 'numeric',
month: "long", month: 'long',
year: "numeric", year: 'numeric'
}); });
// Preparar variáveis com dados formatados // Preparar variáveis com dados formatados
@@ -126,73 +117,65 @@ export function DocumentoEditor({
cliente: { cliente: {
...cliente, ...cliente,
enderecoCompleto, enderecoCompleto,
nome: cliente.nome || "", nome: cliente.nome || '',
cpf: cliente.cpfCnpj || "", // Adicionar cpf como alias para cpfCnpj cpf: cliente.cpfCnpj || '', // Adicionar cpf como alias para cpfCnpj
cpfCnpj: cliente.cpfCnpj || "", // Manter cpfCnpj também cpfCnpj: cliente.cpfCnpj || '', // Manter cpfCnpj também
rg: cliente.rg || "", rg: cliente.rg || '',
estadoCivil: cliente.estadoCivil || "", estadoCivil: cliente.estadoCivil || '',
profissao: cliente.profissao || "", profissao: cliente.profissao || '',
nacionalidade: cliente.nacionalidade || "brasileiro(a)", nacionalidade: cliente.nacionalidade || 'brasileiro(a)',
nomePai: cliente.nomePai || "", nomePai: cliente.nomePai || '',
nomeMae: cliente.nomeMae || "", nomeMae: cliente.nomeMae || ''
}, },
atendimento: atendimentoDetalhado.data, atendimento: atendimentoDetalhado.data,
processo: atendimentoDetalhado.data.processo, processo: atendimentoDetalhado.data.processo,
advogado: { advogado: {
...usuarioRes.data, ...usuarioRes.data,
oabCompleta: `${usuarioRes.data.oab}/${usuarioRes.data.oabUF}`, oabCompleta: `${usuarioRes.data.oab}/${usuarioRes.data.oabUF}`
}, },
escritorio: configRes.data, escritorio: configRes.data,
data: { data: {
atual: new Date().toLocaleDateString("pt-BR"), atual: new Date().toLocaleDateString('pt-BR'),
atualExtenso: formatadorData.format(new Date()), // Formatar a data corretamente atualExtenso: formatadorData.format(new Date()), // Formatar a data corretamente
dia: new Date().getDate().toString(), dia: new Date().getDate().toString(),
mes: (new Date().getMonth() + 1).toString(), mes: (new Date().getMonth() + 1).toString(),
mesExtenso: new Intl.DateTimeFormat("pt-BR", { mesExtenso: new Intl.DateTimeFormat('pt-BR', { month: 'long' }).format(new Date()),
month: "long", ano: new Date().getFullYear().toString()
}).format(new Date()), }
ano: new Date().getFullYear().toString(),
},
}; };
// Teste para debug // Teste para debug
console.log("Data por extenso:", variables.data.atualExtenso); console.log('Data por extenso:', variables.data.atualExtenso);
// Preencher template // Preencher template
const conteudoPreenchido = template.conteudo.replace( const conteudoPreenchido = template.conteudo.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
/\{\{([^}]+)\}\}/g,
(match, path) => {
try { try {
const valor = path const valor = path.split('.').reduce((obj, key) => obj?.[key], variables);
.split(".")
.reduce((obj, key) => obj?.[key], variables);
return valor !== undefined ? valor : match; return valor !== undefined ? valor : match;
} catch (error) { } catch (error) {
console.warn(`Erro ao processar variável ${path}:`, error); console.warn(`Erro ao processar variável ${path}:`, error);
return match; return match;
} }
}, });
);
setContent(conteudoPreenchido); setContent(conteudoPreenchido);
setNome(`${template.nome} - ${cliente.nome}`); setNome(`${template.nome} - ${cliente.nome}`);
setTipo(template.tipo); setTipo(template.tipo);
// Debug para verificar substituições // Debug para verificar substituições
console.log("Template original:", template.conteudo); console.log('Template original:', template.conteudo);
console.log("Conteúdo processado:", conteudoPreenchido); console.log('Conteúdo processado:', conteudoPreenchido);
} catch (error) { } catch (error) {
console.error("Erro ao carregar template:", error); console.error('Erro ao carregar template:', error);
toast.error( toast.error('Erro ao carregar template. Verifique se o atendimento possui um cliente vinculado.');
"Erro ao carregar template. Verifique se o atendimento possui um cliente vinculado.",
);
} }
}; };
const handleSave = async () => { const handleSave = async () => {
// Verificação de atendimento apenas para novos documentos // Verificação de atendimento apenas para novos documentos
if (!documento && !selectedAtendimento) { if (!documento && !selectedAtendimento) {
toast.error("Selecione um atendimento"); toast.error('Selecione um atendimento');
return; return;
} }
@@ -203,12 +186,12 @@ export function DocumentoEditor({
tipo, tipo,
conteudo: content, conteudo: content,
atendimentoId: documento?.atendimentoId || selectedAtendimento, atendimentoId: documento?.atendimentoId || selectedAtendimento,
processoId: documento?.processoId || processoId, processoId: documento?.processoId || processoId
}); });
toast.success("Documento salvo com sucesso"); toast.success('Documento salvo com sucesso');
onClose(); onClose();
} catch (error) { } catch (error) {
toast.error("Erro ao salvar documento"); toast.error('Erro ao salvar documento');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@@ -220,12 +203,12 @@ export function DocumentoEditor({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex gap-4"> <div className="flex gap-4">
<select <select
value={selectedAtendimento || ""} value={selectedAtendimento || ''}
onChange={(e) => setSelectedAtendimento(e.target.value)} onChange={(e) => setSelectedAtendimento(e.target.value)}
className="flex-1 p-2 border rounded" className="flex-1 p-2 border rounded"
> >
<option value="">Selecione um atendimento</option> <option value="">Selecione um atendimento</option>
{atendimentos.map((atendimento) => ( {atendimentos.map(atendimento => (
<option key={atendimento.id} value={atendimento.id}> <option key={atendimento.id} value={atendimento.id}>
{atendimento.protocolo} - {atendimento.cliente.nome} {atendimento.protocolo} - {atendimento.cliente.nome}
</option> </option>
@@ -233,12 +216,12 @@ export function DocumentoEditor({
</select> </select>
<select <select
value={selectedTemplate || ""} value={selectedTemplate || ''}
onChange={(e) => handleTemplateSelect(e.target.value)} onChange={(e) => handleTemplateSelect(e.target.value)}
className="flex-1 p-2 border rounded" className="flex-1 p-2 border rounded"
> >
<option value="">Selecione um template</option> <option value="">Selecione um template</option>
{templates.map((template) => ( {templates.map(template => (
<option key={template.id} value={template.id}> <option key={template.id} value={template.id}>
{template.nome} {template.nome}
</option> </option>
@@ -276,39 +259,23 @@ export function DocumentoEditor({
height: 500, height: 500,
menubar: true, menubar: true,
plugins: [ plugins: [
"advlist", 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
"autolink", 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
"lists", 'insertdatetime', 'media', 'table', 'help', 'wordcount'
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: toolbar: 'undo redo | formatselect | ' +
"undo redo | formatselect | " + 'bold italic backcolor | alignleft aligncenter ' +
"bold italic backcolor | alignleft aligncenter " + 'alignright alignjustify | bullist numlist outdent indent | ' +
"alignright alignjustify | bullist numlist outdent indent | " + 'removeformat | help',
"removeformat | help", language: 'pt_BR', // Adicionar configuração de idioma
language: "pt_BR", // Adicionar configuração de idioma language_url: '/tinymce/langs/pt_BR.js', // Adicionar URL do arquivo de idioma
language_url: "/tinymce/langs/pt_BR.js", // Adicionar URL do arquivo de idioma content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
// Adicionar estas configurações // Adicionar estas configurações
promotion: false, promotion: false,
branding: false, branding: false,
preview_styles: false, preview_styles: false,
referrer_policy: "origin", referrer_policy: 'origin',
disable_stats: true, // Desabilitar estatísticas disable_stats: true // Desabilitar estatísticas
}} }}
value={content} value={content}
onEditorChange={setContent} onEditorChange={setContent}
@@ -331,3 +298,4 @@ export function DocumentoEditor({
</Dialog> </Dialog>
); );
} }
+41 -71
View File
@@ -1,13 +1,8 @@
import { Dialog } from "@headlessui/react"; import { Dialog } from '@headlessui/react';
import { import { XMarkIcon, PrinterIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline';
XMarkIcon, import { api } from '../../lib/api';
PrinterIcon, import { toast } from 'react-toastify';
DocumentArrowDownIcon, import { useState } from 'react';
} from "@heroicons/react/24/outline";
import { useState } from "react";
import { toast } from "react-toastify";
import { api } from "../../lib/api";
interface DocumentoViewerProps { interface DocumentoViewerProps {
documento: any; documento: any;
@@ -15,22 +10,18 @@ interface DocumentoViewerProps {
onClose: () => void; onClose: () => void;
} }
export function DocumentoViewer({ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerProps) {
documento,
isOpen,
onClose,
}: DocumentoViewerProps) {
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const handlePrint = () => { const handlePrint = () => {
const printWindow = window.open("", "_blank"); const printWindow = window.open('', '_blank');
if (!printWindow) return; if (!printWindow) return;
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>${documento?.nome || "Documento"}</title> <title>${documento?.nome || 'Documento'}</title>
<style> <style>
@page { @page {
size: A4; size: A4;
@@ -97,16 +88,16 @@ export function DocumentoViewer({
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h2 style="margin:0;font-size:14pt">${documento?.escritorio?.nome || ""}</h2> <h2 style="margin:0;font-size:14pt">${documento?.escritorio?.nome || ''}</h2>
</div> </div>
<div class="content"> <div class="content">
${documento?.conteudo || ""} ${documento?.conteudo || ''}
</div> </div>
<div class="footer"> <div class="footer">
${documento?.escritorio?.endereco || ""}<br> ${documento?.escritorio?.endereco || ''}<br>
Tel: ${documento?.escritorio?.telefone || ""} | Email: ${documento?.escritorio?.email || ""} Tel: ${documento?.escritorio?.telefone || ''} | Email: ${documento?.escritorio?.email || ''}
</div> </div>
<script> <script>
@@ -126,32 +117,25 @@ export function DocumentoViewer({
const handleExportPDF = async () => { const handleExportPDF = async () => {
try { try {
setIsExporting(true); setIsExporting(true);
toast.info("Gerando PDF, aguarde...", { toast.info('Gerando PDF, aguarde...', { autoClose: false, toastId: 'generating-pdf' });
autoClose: false,
toastId: "generating-pdf",
});
// Buscar configurações atualizadas do escritório // Buscar configurações atualizadas do escritório
const response = await api.post( const response = await api.post('/documentos/exportar-pdf', {
"/documentos/exportar-pdf",
{
conteudo: documento.conteudo, conteudo: documento.conteudo,
escritorio: documento.escritorio, escritorio: documento.escritorio
}, }, {
{ responseType: 'blob',
responseType: "blob",
headers: { headers: {
Accept: "application/pdf", 'Accept': 'application/pdf'
}, }
}, });
);
// Criar e baixar o arquivo // Criar e baixar o arquivo
const blob = new Blob([response.data], { type: "application/pdf" }); const blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute("download", `${documento.nome || "documento"}.pdf`); link.setAttribute('download', `${documento.nome || 'documento'}.pdf`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
@@ -159,19 +143,23 @@ export function DocumentoViewer({
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
document.body.removeChild(link); document.body.removeChild(link);
toast.dismiss("generating-pdf"); toast.dismiss('generating-pdf');
toast.success("PDF gerado com sucesso!"); toast.success('PDF gerado com sucesso!');
} catch (error) { } catch (error) {
console.error("Erro ao gerar PDF:", error); console.error('Erro ao gerar PDF:', error);
toast.dismiss("generating-pdf"); toast.dismiss('generating-pdf');
toast.error("Erro ao gerar o PDF"); toast.error('Erro ao gerar o PDF');
} finally { } finally {
setIsExporting(false); setIsExporting(false);
} }
}; };
return ( return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50"> <Dialog
open={isOpen}
onClose={onClose}
className="relative z-50"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" /> <div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4"> <div className="fixed inset-0 flex items-center justify-center p-4">
@@ -186,31 +174,15 @@ export function DocumentoViewer({
disabled={isExporting} disabled={isExporting}
className={`inline-flex items-center px-3 py-2 ${ className={`inline-flex items-center px-3 py-2 ${
isExporting isExporting
? "bg-blue-400 cursor-not-allowed" ? 'bg-blue-400 cursor-not-allowed'
: "bg-blue-600 hover:bg-blue-700" : 'bg-blue-600 hover:bg-blue-700'
} text-white rounded`} } text-white rounded`}
> >
{isExporting ? ( {isExporting ? (
<> <>
<svg <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
xmlns="http://www.w3.org/2000/svg" <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg> </svg>
Gerando PDF... Gerando PDF...
</> </>
@@ -236,7 +208,7 @@ export function DocumentoViewer({
<div className="max-w-[21cm] mx-auto bg-white shadow-lg p-8"> <div className="max-w-[21cm] mx-auto bg-white shadow-lg p-8">
<div className="text-center mb-8 pb-4 border-b"> <div className="text-center mb-8 pb-4 border-b">
<h2 className="text-xl font-bold"> <h2 className="text-xl font-bold">
{documento?.escritorio?.nome || "Nome do Escritório"} {documento?.escritorio?.nome || 'Nome do Escritório'}
</h2> </h2>
</div> </div>
@@ -246,10 +218,8 @@ export function DocumentoViewer({
/> />
<div className="text-center mt-8 pt-4 border-t text-sm text-gray-600"> <div className="text-center mt-8 pt-4 border-t text-sm text-gray-600">
{documento?.escritorio?.endereco} {documento?.escritorio?.endereco}<br/>
<br /> {documento?.escritorio?.telefone} | {documento?.escritorio?.email}
{documento?.escritorio?.telefone} |{" "}
{documento?.escritorio?.email}
</div> </div>
</div> </div>
</div> </div>
+44 -62
View File
@@ -1,21 +1,13 @@
import { Dialog } from "@headlessui/react"; import { useState, useEffect, useCallback } from 'react';
import { import { useParams, useNavigate, Link } from 'react-router-dom';
DocumentIcon, import { DocumentIcon, PlusIcon, TrashIcon, EyeIcon, CogIcon } from '@heroicons/react/24/outline';
PlusIcon, import { DocumentoEditor } from './DocumentoEditor';
TrashIcon, import { api } from '../lib/api';
EyeIcon, import { toast } from 'react-toastify';
CogIcon, import { useAuth } from '../contexts/AuthContext';
XMarkIcon, import { Dialog } from '@headlessui/react';
} from "@heroicons/react/24/outline"; import { XMarkIcon } from '@heroicons/react/24/outline';
import { useState, useEffect, useCallback } from "react"; import { DocumentoViewer } from './DocumentoViewer';
import { useParams, useNavigate, Link } from "react-router-dom";
import { toast } from "react-toastify";
import { useAuth } from "../contexts/AuthContext";
import { api } from "../lib/api";
import { DocumentoEditor } from "./DocumentoEditor";
import { DocumentoViewer } from "./DocumentoViewer";
export function DocumentosPage() { export function DocumentosPage() {
const [isEditorOpen, setIsEditorOpen] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false);
@@ -31,7 +23,7 @@ export function DocumentosPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!user) { if (!user) {
navigate("/login"); navigate('/login');
return; return;
} }
@@ -39,12 +31,12 @@ export function DocumentosPage() {
setIsLoading(true); setIsLoading(true);
await Promise.all([loadDocumentos(), loadModelos()]); await Promise.all([loadDocumentos(), loadModelos()]);
} catch (error) { } catch (error) {
console.error("Erro ao carregar dados:", error); console.error('Erro ao carregar dados:', error);
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
signOut(); // Usar a função do AuthContext signOut(); // Usar a função do AuthContext
navigate("/login"); navigate('/login');
} else { } else {
toast.error("Erro ao carregar dados"); toast.error('Erro ao carregar dados');
} }
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -60,12 +52,12 @@ export function DocumentosPage() {
if (processoId) params.processoId = processoId; if (processoId) params.processoId = processoId;
if (atendimentoId) params.atendimentoId = atendimentoId; if (atendimentoId) params.atendimentoId = atendimentoId;
const response = await api.get("/documentos", { params }); const response = await api.get('/documentos', { params });
setDocumentos(response.data); setDocumentos(response.data);
}; };
const loadModelos = async () => { const loadModelos = async () => {
const response = await api.get("/documentos-modelos"); const response = await api.get('/documentos-modelos');
setModelos(response.data); setModelos(response.data);
}; };
@@ -74,16 +66,16 @@ export function DocumentosPage() {
if (processoId) data.processoId = processoId; if (processoId) data.processoId = processoId;
if (atendimentoId) data.atendimentoId = atendimentoId; if (atendimentoId) data.atendimentoId = atendimentoId;
await api.post("/documentos", data); await api.post('/documentos', data);
toast.success("Documento salvo com sucesso"); toast.success('Documento salvo com sucesso');
await loadDocumentos(); await loadDocumentos();
setIsEditorOpen(false); setIsEditorOpen(false);
} catch (error) { } catch (error) {
console.error("Erro ao salvar documento:", error); console.error('Erro ao salvar documento:', error);
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
toast.error("Sessão expirada. Por favor, faça login novamente."); toast.error('Sessão expirada. Por favor, faça login novamente.');
} else { } else {
toast.error("Erro ao salvar documento"); toast.error('Erro ao salvar documento');
} }
} }
}; };
@@ -91,30 +83,30 @@ export function DocumentosPage() {
const handleDeleteDocumento = async (id) => { const handleDeleteDocumento = async (id) => {
try { try {
await api.delete(`/documentos/${id}`); await api.delete(`/documentos/${id}`);
toast.success("Documento excluído com sucesso"); toast.success('Documento excluído com sucesso');
await loadDocumentos(); await loadDocumentos();
} catch (error) { } catch (error) {
console.error("Erro ao excluir documento:", error); console.error('Erro ao excluir documento:', error);
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
toast.error("Sessão expirada. Por favor, faça login novamente."); toast.error('Sessão expirada. Por favor, faça login novamente.');
} else { } else {
toast.error("Erro ao excluir documento"); toast.error('Erro ao excluir documento');
} }
} }
}; };
const handleVisualizar = async (documento) => { const handleVisualizar = async (documento) => {
try { try {
const configRes = await api.get("/documentos/configuracoes"); const configRes = await api.get('/documentos/configuracoes');
setDocumentoVisualizado({ setDocumentoVisualizado({
...documento, ...documento,
escritorio: configRes.data, escritorio: configRes.data
}); });
setVisualizadorOpen(true); setVisualizadorOpen(true);
} catch (error) { } catch (error) {
console.error("Erro ao visualizar documento:", error); console.error('Erro ao visualizar documento:', error);
toast.error("Erro ao visualizar documento"); toast.error('Erro ao visualizar documento');
} }
}; };
@@ -134,17 +126,17 @@ export function DocumentosPage() {
<PlusIcon className="w-5 h-5 mr-2" /> <PlusIcon className="w-5 h-5 mr-2" />
Novo Documento Novo Documento
</button> </button>
{user?.perfil === "ADVOGADO_ADMIN" && ( {user?.perfil === 'ADVOGADO_ADMIN' && (
<> <>
<button <button
onClick={() => navigate("/documentos/templates")} onClick={() => navigate('/documentos/templates')}
className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors"
> >
<DocumentIcon className="w-5 h-5 mr-2" /> <DocumentIcon className="w-5 h-5 mr-2" />
Gerenciar Templates Gerenciar Templates
</button> </button>
<button <button
onClick={() => navigate("/documentos/configuracoes")} onClick={() => navigate('/documentos/configuracoes')}
className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors"
> >
<CogIcon className="w-5 h-5 mr-2" /> <CogIcon className="w-5 h-5 mr-2" />
@@ -161,8 +153,8 @@ export function DocumentosPage() {
onClick={() => setTabValue(0)} onClick={() => setTabValue(0)}
className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${ className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 0 tabValue === 0
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Documentos Documentos
@@ -171,8 +163,8 @@ export function DocumentosPage() {
onClick={() => setTabValue(1)} onClick={() => setTabValue(1)}
className={`ml-8 px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${ className={`ml-8 px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 1 tabValue === 1
? "border-blue-500 text-blue-600" ? 'border-blue-500 text-blue-600'
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300" : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
Modelos Modelos
@@ -183,16 +175,11 @@ export function DocumentosPage() {
{tabValue === 0 && ( {tabValue === 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{documentos.map((doc) => ( {documentos.map((doc) => (
<div <div key={doc.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
key={doc.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<div className="p-4"> <div className="p-4">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" /> <DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100"> <h3 className="font-semibold text-gray-900 dark:text-gray-100">{doc.nome}</h3>
{doc.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Criado em: {new Date(doc.criadoEm).toLocaleDateString()} Criado em: {new Date(doc.criadoEm).toLocaleDateString()}
@@ -222,21 +209,16 @@ export function DocumentosPage() {
{tabValue === 1 && ( {tabValue === 1 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modelos.map((modelo) => ( {modelos.map((modelo) => (
<div <div key={modelo.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
key={modelo.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<div className="p-4"> <div className="p-4">
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" /> <DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100"> <h3 className="font-semibold text-gray-900 dark:text-gray-100">{modelo.nome}</h3>
{modelo.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{modelo.descricao}</p>
{modelo.descricao} <button
</p> className="w-full inline-flex items-center justify-center px-4 py-2 text-sm border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20"
<button className="w-full inline-flex items-center justify-center px-4 py-2 text-sm border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20"> >
Usar Modelo Usar Modelo
</button> </button>
</div> </div>
+61 -107
View File
@@ -1,16 +1,10 @@
import { Dialog } from "@headlessui/react"; import { useState, useEffect } from 'react';
import { import { api } from '../../lib/api';
DocumentIcon, import { Editor } from '@tinymce/tinymce-react';
PencilIcon, import { toast } from 'react-toastify';
TrashIcon, import { Dialog } from '@headlessui/react';
PlusIcon, import { DocumentIcon, PencilIcon, TrashIcon, PlusIcon } from '@heroicons/react/24/outline';
} from "@heroicons/react/24/outline"; import { DOCUMENT_VARIABLES } from '../../utils/documentVariables';
import { Editor } from "@tinymce/tinymce-react";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
import { api } from "../../lib/api";
import { DOCUMENT_VARIABLES } from "../../utils/documentVariables";
export function GerenciarTemplates() { export function GerenciarTemplates() {
const [templates, setTemplates] = useState([]); const [templates, setTemplates] = useState([]);
@@ -18,19 +12,19 @@ export function GerenciarTemplates() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditorModalOpen, setEditorModalOpen] = useState(false); const [isEditorModalOpen, setEditorModalOpen] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nome: "", nome: '',
tipo: "OUTROS", tipo: 'OUTROS',
descricao: "", descricao: '',
conteudo: "", conteudo: '',
variaveis: [], variaveis: []
}); });
const loadTemplates = async () => { const loadTemplates = async () => {
try { try {
const response = await api.get("/documentos-modelos"); const response = await api.get('/documentos-modelos');
setTemplates(response.data); setTemplates(response.data);
} catch (error) { } catch (error) {
toast.error("Erro ao carregar templates"); toast.error('Erro ao carregar templates');
} }
}; };
@@ -42,15 +36,15 @@ export function GerenciarTemplates() {
try { try {
if (editingTemplate) { if (editingTemplate) {
await api.put(`/documentos-modelos/${editingTemplate.id}`, data); await api.put(`/documentos-modelos/${editingTemplate.id}`, data);
toast.success("Template atualizado com sucesso"); toast.success('Template atualizado com sucesso');
} else { } else {
await api.post("/documentos-modelos", data); await api.post('/documentos-modelos', data);
toast.success("Template criado com sucesso"); toast.success('Template criado com sucesso');
} }
loadTemplates(); loadTemplates();
setIsModalOpen(false); setIsModalOpen(false);
} catch (error) { } catch (error) {
toast.error("Erro ao salvar template"); toast.error('Erro ao salvar template');
} }
}; };
@@ -61,35 +55,35 @@ export function GerenciarTemplates() {
if (editingTemplate) { if (editingTemplate) {
await api.put(`/documentos-modelos/${editingTemplate.id}`, { await api.put(`/documentos-modelos/${editingTemplate.id}`, {
...formData, ...formData,
id: editingTemplate.id, // Garantir que o ID seja enviado id: editingTemplate.id // Garantir que o ID seja enviado
}); });
toast.success("Template atualizado com sucesso"); toast.success('Template atualizado com sucesso');
} else { } else {
await api.post("/documentos-modelos", formData); await api.post('/documentos-modelos', formData);
toast.success("Template criado com sucesso"); toast.success('Template criado com sucesso');
} }
setEditorModalOpen(false); setEditorModalOpen(false);
setEditingTemplate(null); setEditingTemplate(null);
await loadTemplates(); // Recarregar a lista após salvar await loadTemplates(); // Recarregar a lista após salvar
} catch (error) { } catch (error) {
console.error("Erro ao salvar template:", error); console.error('Erro ao salvar template:', error);
toast.error("Erro ao salvar template"); toast.error('Erro ao salvar template');
} }
}; };
const handleDelete = async (templateId: string) => { const handleDelete = async (templateId: string) => {
if (!window.confirm("Tem certeza que deseja excluir este template?")) { if (!window.confirm('Tem certeza que deseja excluir este template?')) {
return; return;
} }
try { try {
await api.delete(`/documentos-modelos/${templateId}`); await api.delete(`/documentos-modelos/${templateId}`);
toast.success("Template excluído com sucesso"); toast.success('Template excluído com sucesso');
loadTemplates(); loadTemplates();
} catch (error) { } catch (error) {
console.error("Erro ao excluir template:", error); console.error('Erro ao excluir template:', error);
toast.error("Erro ao excluir template"); toast.error('Erro ao excluir template');
} }
}; };
@@ -104,7 +98,7 @@ export function GerenciarTemplates() {
setEditingTemplate(template); // Guardar o template completo setEditingTemplate(template); // Guardar o template completo
setFormData({ setFormData({
...template, ...template,
id: template.id, // Garantir que o ID seja mantido id: template.id // Garantir que o ID seja mantido
}); });
setEditorModalOpen(true); setEditorModalOpen(true);
}; };
@@ -116,11 +110,11 @@ export function GerenciarTemplates() {
<button <button
onClick={() => { onClick={() => {
setFormData({ setFormData({
nome: "", nome: '',
tipo: "OUTROS", tipo: 'OUTROS',
descricao: "", descricao: '',
conteudo: "", conteudo: '',
variaveis: [], variaveis: []
}); });
setEditorModalOpen(true); setEditorModalOpen(true);
}} }}
@@ -133,17 +127,12 @@ export function GerenciarTemplates() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templates.map((template) => ( {templates.map((template) => (
<div <div key={template.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
key={template.id}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-4"
>
<div className="flex items-center mb-3"> <div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" /> <DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold">{template.nome}</h3> <h3 className="font-semibold">{template.nome}</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{template.descricao}</p>
{template.descricao}
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => handleEdit(template)} onClick={() => handleEdit(template)}
@@ -165,18 +154,14 @@ export function GerenciarTemplates() {
</div> </div>
{/* Modal de Edição */} {/* Modal de Edição */}
<Dialog <Dialog open={isEditorModalOpen} onClose={() => setEditorModalOpen(false)} className="relative z-50">
open={isEditorModalOpen}
onClose={() => setEditorModalOpen(false)}
className="relative z-50"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" /> <div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4 overflow-y-auto"> <div className="fixed inset-0 flex items-center justify-center p-4 overflow-y-auto">
<Dialog.Panel className="w-full max-w-4xl bg-white rounded-lg shadow-xl my-4"> <Dialog.Panel className="w-full max-w-4xl bg-white rounded-lg shadow-xl my-4">
<div className="max-h-[90vh] overflow-y-auto"> <div className="max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSubmit} className="p-6"> <form onSubmit={handleSubmit} className="p-6">
<h2 className="text-xl font-bold mb-4"> <h2 className="text-xl font-bold mb-4">
{editingTemplate ? "Editar Template" : "Novo Template"} {editingTemplate ? 'Editar Template' : 'Novo Template'}
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -185,9 +170,7 @@ export function GerenciarTemplates() {
<input <input
type="text" type="text"
value={formData.nome} value={formData.nome}
onChange={(e) => onChange={(e) => setFormData({...formData, nome: e.target.value})}
setFormData({ ...formData, nome: e.target.value })
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
required required
/> />
@@ -197,9 +180,7 @@ export function GerenciarTemplates() {
<label className="block mb-1">Tipo</label> <label className="block mb-1">Tipo</label>
<select <select
value={formData.tipo} value={formData.tipo}
onChange={(e) => onChange={(e) => setFormData({...formData, tipo: e.target.value})}
setFormData({ ...formData, tipo: e.target.value })
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
> >
<option value="PETICAO">Petição</option> <option value="PETICAO">Petição</option>
@@ -214,9 +195,7 @@ export function GerenciarTemplates() {
<label className="block mb-1">Descrição</label> <label className="block mb-1">Descrição</label>
<textarea <textarea
value={formData.descricao} value={formData.descricao}
onChange={(e) => onChange={(e) => setFormData({...formData, descricao: e.target.value})}
setFormData({ ...formData, descricao: e.target.value })
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
rows={3} rows={3}
/> />
@@ -225,27 +204,21 @@ export function GerenciarTemplates() {
<div className="mb-4"> <div className="mb-4">
<label className="block mb-1">Variáveis Disponíveis</label> <label className="block mb-1">Variáveis Disponíveis</label>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 p-2 border rounded bg-gray-50"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 p-2 border rounded bg-gray-50">
{Object.entries(DOCUMENT_VARIABLES).map( {Object.entries(DOCUMENT_VARIABLES).map(([category, variables]) => (
([category, variables]) => (
<div key={category} className="space-y-1"> <div key={category} className="space-y-1">
<h4 className="font-bold text-sm text-gray-700"> <h4 className="font-bold text-sm text-gray-700">{category}</h4>
{category} {Object.entries(variables).map(([name, variable]) => (
</h4>
{Object.entries(variables).map(
([name, variable]) => (
<button <button
key={name} key={name}
type="button" type="button"
onClick={() => insertVariable(variable)} onClick={() => insertVariable(variable)}
className="text-xs text-blue-600 hover:text-blue-800 block" className="text-xs text-blue-600 hover:text-blue-800 block"
> >
{name.replace(/_/g, " ")} {name.replace(/_/g, ' ')}
</button> </button>
), ))}
)}
</div> </div>
), ))}
)}
</div> </div>
</div> </div>
@@ -254,50 +227,31 @@ export function GerenciarTemplates() {
<Editor <Editor
apiKey={import.meta.env.VITE_TINYMCE_API_KEY} apiKey={import.meta.env.VITE_TINYMCE_API_KEY}
value={formData.conteudo} value={formData.conteudo}
onEditorChange={(content) => onEditorChange={(content) => setFormData({...formData, conteudo: content})}
setFormData({ ...formData, conteudo: content })
}
init={{ init={{
height: 400, height: 400,
menubar: true, menubar: true,
plugins: [ plugins: [
"advlist", 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
"autolink", 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
"lists", 'insertdatetime', 'media', 'table', 'help', 'wordcount'
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: toolbar: 'variables | undo redo | formatselect | bold italic | alignleft aligncenter alignright',
"variables | undo redo | formatselect | bold italic | alignleft aligncenter alignright",
setup: (editor) => { setup: (editor) => {
editor.ui.registry.addMenuButton("variables", { editor.ui.registry.addMenuButton('variables', {
text: "Variáveis", text: 'Variáveis',
fetch: (callback) => { fetch: (callback) => {
const items = Object.entries( const items = Object.entries(DOCUMENT_VARIABLES).flatMap(([category, vars]) =>
DOCUMENT_VARIABLES,
).flatMap(([category, vars]) =>
Object.entries(vars).map(([name, value]) => ({ Object.entries(vars).map(([name, value]) => ({
type: "menuitem", type: 'menuitem',
text: `${category} - ${name.replace(/_/g, " ")}`, text: `${category} - ${name.replace(/_/g, ' ')}`,
onAction: () => editor.insertContent(value), onAction: () => editor.insertContent(value)
})), }))
); );
callback(items); callback(items);
}, }
}); });
}, }
}} }}
/> />
</div> </div>
+12 -11
View File
@@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from 'react';
import { DocumentoService } from '../../services/DocumentoService';
import { DocumentoService } from "../../services/DocumentoService"; import { TIPOS_DOCUMENTO } from '../../utils/documentTemplates';
import { TIPOS_DOCUMENTO } from "../../utils/documentTemplates";
export function NovoDocumento() { export function NovoDocumento() {
const [tipo, setTipo] = useState(TIPOS_DOCUMENTO.PROCURACAO); const [tipo, setTipo] = useState(TIPOS_DOCUMENTO.PROCURACAO);
@@ -9,24 +8,24 @@ export function NovoDocumento() {
const handleGerar = async () => { const handleGerar = async () => {
// 1. Buscar dados necessários // 1. Buscar dados necessários
const cliente = await api.get(`/clientes/${clienteId}`); const cliente = await api.get(`/clientes/${clienteId}`);
const advogado = await api.get("/usuarios/me"); const advogado = await api.get('/usuarios/me');
// 2. Preparar variáveis // 2. Preparar variáveis
const variaveis = { const variaveis = {
cliente: cliente.data, cliente: cliente.data,
advogado: advogado.data, advogado: advogado.data,
cidade: "São Paulo", cidade: 'São Paulo',
data: new Date().toLocaleDateString(), data: new Date().toLocaleDateString()
}; };
// 3. Buscar configurações do escritório // 3. Buscar configurações do escritório
const config = await api.get("/configuracoes"); const config = await api.get('/configuracoes');
// 4. Gerar documento // 4. Gerar documento
const documento = DocumentoService.gerarDocumento({ const documento = DocumentoService.gerarDocumento({
tipo, tipo,
variaveis, variaveis,
config: config.data, config: config.data
}); });
// 5. Salvar ou exibir // 5. Salvar ou exibir
@@ -35,13 +34,15 @@ export function NovoDocumento() {
return ( return (
<div> <div>
<select value={tipo} onChange={(e) => setTipo(e.target.value)}> <select value={tipo} onChange={e => setTipo(e.target.value)}>
<option value={TIPOS_DOCUMENTO.PROCURACAO}>Procuração</option> <option value={TIPOS_DOCUMENTO.PROCURACAO}>Procuração</option>
<option value={TIPOS_DOCUMENTO.CONTRATO}>Contrato</option> <option value={TIPOS_DOCUMENTO.CONTRATO}>Contrato</option>
{/* ... */} {/* ... */}
</select> </select>
<button onClick={handleGerar}>Gerar Documento</button> <button onClick={handleGerar}>
Gerar Documento
</button>
</div> </div>
); );
} }
+2 -2
View File
@@ -8,7 +8,7 @@ interface FormInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>( export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
({ label, error, className = "", multiline, ...props }, ref) => { ({ label, error, className = "", multiline, ...props }, ref) => {
const Component = multiline ? "textarea" : "input"; const Component = multiline ? 'textarea' : 'input';
return ( return (
<div className={`${className} relative`}> <div className={`${className} relative`}>
@@ -37,7 +37,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
)} )}
</div> </div>
); );
}, }
); );
FormInput.displayName = "FormInput"; FormInput.displayName = "FormInput";
+4 -10
View File
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from 'react';
export const FormInput = forwardRef<HTMLInputElement, any>( export const FormInput = forwardRef<HTMLInputElement, any>(
({ label, error, className = "", ...props }, ref) => ( ({ label, error, className = "", ...props }, ref) => (
@@ -26,7 +26,7 @@ export const FormInput = forwardRef<HTMLInputElement, any>(
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{error}</p> <p className="mt-1 text-sm text-red-600 dark:text-red-400">{error}</p>
)} )}
</div> </div>
), )
); );
export const FormSelect = forwardRef<HTMLSelectElement, any>( export const FormSelect = forwardRef<HTMLSelectElement, any>(
@@ -52,15 +52,9 @@ export const FormSelect = forwardRef<HTMLSelectElement, any>(
`} `}
{...props} {...props}
> >
<option value="" className="dark:bg-gray-700"> <option value="" className="dark:bg-gray-700">Selecione...</option>
Selecione...
</option>
{options?.map((opt: any) => ( {options?.map((opt: any) => (
<option <option key={opt.value} value={opt.value} className="dark:bg-gray-700">
key={opt.value}
value={opt.value}
className="dark:bg-gray-700"
>
{opt.label} {opt.label}
</option> </option>
))} ))}
+62 -65
View File
@@ -1,4 +1,6 @@
import { Menu, Transition } from "@headlessui/react"; import React, { useState, Fragment, useEffect, useRef } from "react";
import { Outlet, Link, useLocation } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import { import {
BellIcon, BellIcon,
ChevronDownIcon, ChevronDownIcon,
@@ -15,19 +17,18 @@ import {
ShieldCheckIcon, ShieldCheckIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
CalculatorIcon, CalculatorIcon,
BriefcaseIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import CalculateIcon from "@mui/icons-material/Calculate"; import { Menu, Transition } from "@headlessui/react";
import ChildCareIcon from "@mui/icons-material/ChildCare";
import MoneyIcon from "@mui/icons-material/Money";
import React, { useState, Fragment, useEffect, useRef } from "react";
import { Outlet, Link, useLocation } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import { useTheme } from "../../contexts/ThemeContext";
import api from "../../services/api"; import api from "../../services/api";
import { useTheme } from "../../contexts/ThemeContext";
// Importando o ícone para cálculos judiciais // Importando o ícone para cálculos judiciais
import CalculateIcon from '@mui/icons-material/Calculate';
// Importar ícones específicos para subcategorias de cálculos // Importar ícones específicos para subcategorias de cálculos
import MoneyIcon from '@mui/icons-material/Money';
import ChildCareIcon from '@mui/icons-material/ChildCare';
export const MainLayout: React.FC = () => { export const MainLayout: React.FC = () => {
const [isSidebarOpen, setSidebarOpen] = useState(true); const [isSidebarOpen, setSidebarOpen] = useState(true);
@@ -59,57 +60,65 @@ export const MainLayout: React.FC = () => {
const menuItems = [ const menuItems = [
{ path: "/dashboard", label: "Dashboard", icon: HomeIcon }, { path: "/dashboard", label: "Dashboard", icon: HomeIcon },
{ path: "/processos", label: "Processos", icon: ScaleIcon }, {
{ path: "/processos-administrativos", label: "Proc.Adm", icon: ScaleIcon }, path: null,
label: "Processos",
icon: ScaleIcon,
subitems: [
{ path: "/processos", label: "Judiciais", icon: ScaleIcon },
{ path: "/processos-administrativos", label: "Proc. Admin.", icon: DocumentCheckIcon }
]
},
{ {
path: null, path: null,
label: "Provas", label: "Provas",
icon: DocumentCheckIcon, icon: DocumentCheckIcon,
subitems: [ subitems: [
{ {
path: "/provas/upload", path: '/provas/upload',
label: "Upload", label: 'Upload',
roles: [], roles: [],
icon: ArrowUpTrayIcon, icon: ArrowUpTrayIcon
}, },
{ {
path: "/provas/admin", path: '/provas/admin',
label: "Admin", label: 'Admin',
roles: ["ADVOGADO_ADMIN"], roles: ['ADVOGADO_ADMIN'],
icon: ShieldCheckIcon, icon: ShieldCheckIcon
}, }
], ]
}, },
{ path: "/clientes", label: "Clientes", icon: UserGroupIcon }, { path: "/clientes", label: "Clientes", icon: UserGroupIcon },
{ path: "/atendimentos", label: "Atendimentos", icon: UserIcon }, { path: "/atendimentos", label: "Atendimentos", icon: UserIcon },
{ path: "/tarefas", label: "Tarefas", icon: ClipboardDocumentCheckIcon }, { path: "/tarefas", label: "Tarefas", icon: ClipboardDocumentCheckIcon },
{ path: "/documentos", label: "Documentos", icon: DocumentTextIcon }, { path: "/documentos", label: "Documentos", icon: DocumentTextIcon },
{ path: "/pdf", label: "PDFs", icon: DocumentDuplicateIcon },
{ {
path: null, path: null,
label: "Cálculos", label: "Cálculos",
icon: CalculateIcon, icon: CalculateIcon,
roles: ["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"], roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
subitems: [ subitems: [
{ {
path: "/calculos", path: '/calculos',
label: "Dashboard", label: 'Dashboard',
roles: ["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"], roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
icon: HomeIcon, icon: HomeIcon
}, },
{ {
path: "/calculos/pensao-alimenticia", path: '/calculos/pensao-alimenticia',
label: "Pensão Alimentícia", label: 'Pensão Alimentícia',
roles: ["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"], roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
icon: ChildCareIcon, icon: ChildCareIcon
}, },
{ {
path: "/calculos/trabalhistas", path: '/calculos/trabalhistas',
label: "Trabalhistas", label: 'Trabalhistas',
roles: ["ADVOGADO_ADMIN", "ADVOGADO", "ASSISTENTE"], roles: ['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE'],
icon: MoneyIcon, icon: MoneyIcon
}, }
], ]
}, }
]; ];
// Função para verificar se o caminho atual corresponde ao item do menu // Função para verificar se o caminho atual corresponde ao item do menu
@@ -129,10 +138,7 @@ export const MainLayout: React.FC = () => {
const menuItem = document.querySelector(`[data-menu-label="${menuLabel}"]`); const menuItem = document.querySelector(`[data-menu-label="${menuLabel}"]`);
if (menuItem) { if (menuItem) {
const rect = menuItem.getBoundingClientRect(); const rect = menuItem.getBoundingClientRect();
document.documentElement.style.setProperty( document.documentElement.style.setProperty('--menu-item-top', `${rect.top}px`);
"--menu-item-top",
`${rect.top}px`,
);
} }
}; };
@@ -173,7 +179,10 @@ export const MainLayout: React.FC = () => {
<nav className="flex-1 overflow-y-auto"> <nav className="flex-1 overflow-y-auto">
<div className="py-4"> <div className="py-4">
{menuItems.map((item, index) => ( {menuItems.map((item, index) => (
<div key={item.path || item.label} className="px-2 mb-2"> <div
key={item.path || item.label}
className="px-2 mb-2"
>
{!item.subitems ? ( {!item.subitems ? (
<Link <Link
to={item.path} to={item.path}
@@ -185,9 +194,7 @@ export const MainLayout: React.FC = () => {
title={item.label} title={item.label}
> >
<item.icon className="h-6 w-6 text-white opacity-80" /> <item.icon className="h-6 w-6 text-white opacity-80" />
<span className="text-xs mt-1 text-white/80"> <span className="text-xs mt-1 text-white/80">{item.label}</span>
{item.label}
</span>
</Link> </Link>
) : ( ) : (
<div <div
@@ -198,18 +205,14 @@ export const MainLayout: React.FC = () => {
> >
<div <div
className={`flex flex-col items-center justify-center p-3 rounded-lg transition-colors hover:bg-white/10 cursor-pointer ${ className={`flex flex-col items-center justify-center p-3 rounded-lg transition-colors hover:bg-white/10 cursor-pointer ${
item.subitems.some((subitem) => item.subitems.some(subitem => isCurrentPath(subitem.path))
isCurrentPath(subitem.path),
)
? "bg-white/20" ? "bg-white/20"
: "" : ""
}`} }`}
title={item.label} title={item.label}
> >
<item.icon className="h-6 w-6 text-white opacity-80" /> <item.icon className="h-6 w-6 text-white opacity-80" />
<span className="text-xs mt-1 text-white/80"> <span className="text-xs mt-1 text-white/80">{item.label}</span>
{item.label}
</span>
</div> </div>
</div> </div>
)} )}
@@ -220,26 +223,24 @@ export const MainLayout: React.FC = () => {
{/* Submenus renderizados fora da estrutura do menu principal */} {/* Submenus renderizados fora da estrutura do menu principal */}
{menuItems {menuItems
.filter((item) => item.subitems) .filter(item => item.subitems)
.map((item) => ( .map(item => (
<div <div
key={`submenu-${item.label}`} key={`submenu-${item.label}`}
className={`fixed left-20 z-50 ${activeSubmenu === item.label ? "block" : "hidden"}`} className={`fixed left-20 z-50 ${activeSubmenu === item.label ? 'block' : 'hidden'}`}
style={{ top: "var(--menu-item-top, 0)" }} style={{ top: 'var(--menu-item-top, 0)' }}
onMouseEnter={handleSubmenuMouseEnter} onMouseEnter={handleSubmenuMouseEnter}
onMouseLeave={handleMenuMouseLeave} onMouseLeave={handleMenuMouseLeave}
> >
<div <div
className="transition-all duration-200 ml-2" className="transition-all duration-200 ml-2"
style={{ transformOrigin: "left center" }} style={{ transformOrigin: 'left center' }}
> >
<div className="bg-blue-800/95 backdrop-blur-sm rounded-lg py-2 px-1 shadow-xl w-[140px] border border-blue-700/30"> <div className="bg-blue-800/95 backdrop-blur-sm rounded-lg py-2 px-1 shadow-xl w-[140px] border border-blue-700/30">
{item.subitems {item.subitems
.filter( .filter(subitem =>
(subitem) =>
!subitem.roles?.length || !subitem.roles?.length ||
(userProfile?.perfil && (userProfile?.perfil && subitem.roles.includes(userProfile.perfil))
subitem.roles.includes(userProfile.perfil)),
) )
.map((subitem) => ( .map((subitem) => (
<Link <Link
@@ -251,9 +252,7 @@ export const MainLayout: React.FC = () => {
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<subitem.icon className="h-5 w-5 text-white opacity-80" /> <subitem.icon className="h-5 w-5 text-white opacity-80" />
<span className="text-sm text-white/90"> <span className="text-sm text-white/90">{subitem.label}</span>
{subitem.label}
</span>
</div> </div>
</Link> </Link>
))} ))}
@@ -306,9 +305,7 @@ export const MainLayout: React.FC = () => {
<div className="text-sm font-medium text-gray-700 dark:text-gray-200"> <div className="text-sm font-medium text-gray-700 dark:text-gray-200">
{user?.nome} {user?.nome}
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">{user?.cargo}</div>
{user?.cargo}
</div>
</div> </div>
<ChevronDownIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" /> <ChevronDownIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" />
</Menu.Button> </Menu.Button>
+885
View File
@@ -0,0 +1,885 @@
/// <reference types="react" />
import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import axios from 'axios';
import { toast } from 'react-toastify';
import { useAuth } from '../../contexts/AuthContext';
import {
DocumentIcon,
CloudArrowUpIcon,
ArrowsPointingInIcon,
ScissorsIcon,
ArrowsUpDownIcon,
PhotoIcon,
DocumentTextIcon,
InformationCircleIcon,
ArrowDownTrayIcon,
ClockIcon,
} from '@heroicons/react/24/outline';
interface OperationResult {
operacaoId: string;
outputPaths?: string[];
outputPath?: string;
zipPath?: string;
processingTime?: number;
reduction?: number;
filesCreated?: number;
text?: string;
confidence?: number;
}
type OperationType =
| 'compress'
| 'split'
| 'merge'
| 'pdfToImages'
| 'imagesToPDF'
| 'ocr'
| 'info';
export const PDFManipulationPage: React.FC = () => {
const { getToken } = useAuth();
const [selectedOperation, setSelectedOperation] = useState<OperationType>('compress');
const [files, setFiles] = useState<File[]>([]);
const [processing, setProcessing] = useState(false);
const [result, setResult] = useState<OperationResult | null>(null);
const [history, setHistory] = useState<any[]>([]);
// Configurações para cada operação
const [compressQuality, setCompressQuality] = useState<'extreme' | 'low' | 'medium' | 'high'>('medium');
const [enableOCR, setEnableOCR] = useState(false);
const [splitType, setSplitType] = useState<'pages' | 'range' | 'chunks'>('pages');
const [splitPages, setSplitPages] = useState<string>('1,2,3');
const [splitRanges, setSplitRanges] = useState<string>('1-5,6-10');
const [chunkSize, setChunkSize] = useState(5);
const [imageFormat, setImageFormat] = useState<'png' | 'jpg' | 'webp'>('png');
const [imageQuality, setImageQuality] = useState(95);
const [imageDensity, setImageDensity] = useState(300);
const [ocrLanguage, setOcrLanguage] = useState('por');
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles((prev) => [...prev, ...acceptedFiles]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf'],
'image/*': ['.png', '.jpg', '.jpeg', '.webp'],
},
});
const removeFile = (index: number) => {
setFiles((prev) => prev.filter((_, i) => i !== index));
};
const clearFiles = () => {
setFiles([]);
setResult(null);
};
const processFiles = async () => {
if (files.length === 0) {
toast.error('Selecione pelo menos um arquivo');
return;
}
setProcessing(true);
setResult(null);
try {
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
const token = getToken();
const config = {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
timeout: 600000, // 10 minutos para operações longas (compressão, OCR, etc)
};
let response;
switch (selectedOperation) {
case 'compress':
formData.append('quality', compressQuality);
formData.append('enableOCR', enableOCR.toString());
response = await axios.post('/api/pdf/compress', formData, config);
toast.success('PDFs comprimidos com sucesso!');
break;
case 'split':
formData.append('splitType', splitType);
if (splitType === 'pages') {
const pages = splitPages.split(',').map((p) => parseInt(p.trim()));
formData.append('pages', JSON.stringify(pages));
} else if (splitType === 'range') {
const ranges = splitRanges.split(',').map((r) => {
const [start, end] = r.trim().split('-').map(Number);
return { start, end };
});
formData.append('ranges', JSON.stringify(ranges));
} else {
formData.append('chunkSize', chunkSize.toString());
}
response = await axios.post('/api/pdf/split', formData, config);
toast.success('PDF dividido com sucesso!');
break;
case 'merge':
formData.append('enableOCR', enableOCR.toString());
response = await axios.post('/api/pdf/merge', formData, config);
toast.success('PDFs mesclados com sucesso!');
break;
case 'pdfToImages':
formData.append('format', imageFormat);
formData.append('quality', imageQuality.toString());
formData.append('density', imageDensity.toString());
response = await axios.post('/api/pdf/pdf-to-images', formData, config);
toast.success('PDF convertido em imagens!');
break;
case 'imagesToPDF':
formData.append('enableOCR', enableOCR.toString());
formData.append('fitToPage', 'true');
response = await axios.post('/api/pdf/images-to-pdf', formData, config);
toast.success('Imagens convertidas em PDF!');
break;
case 'ocr':
formData.append('language', ocrLanguage);
response = await axios.post('/api/pdf/ocr', formData, config);
toast.success('OCR realizado com sucesso!');
break;
case 'info':
response = await axios.post('/api/pdf/info', formData, config);
break;
}
setResult(response.data);
loadHistory();
} catch (error: any) {
console.error('Erro ao processar arquivos:', error);
toast.error(error.response?.data?.message || 'Erro ao processar arquivos');
} finally {
setProcessing(false);
}
};
const loadHistory = async () => {
try {
const token = getToken();
const response = await axios.get('/api/pdf/history', {
headers: { Authorization: `Bearer ${token}` },
});
setHistory(response.data.operacoes || []);
} catch (error) {
console.error('Erro ao carregar histórico:', error);
}
};
const downloadFile = async (operacaoId: string, fileIndex: number) => {
try {
const token = getToken();
const response = await axios.get(`/api/pdf/download/${operacaoId}/${fileIndex}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob',
});
// Extrai o nome do arquivo do header Content-Disposition
const contentDisposition = response.headers['content-disposition'];
let fileName = `arquivo-${fileIndex}`;
if (contentDisposition) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
if (matches != null && matches[1]) {
fileName = matches[1].replace(/['"]/g, '');
}
}
// Usa o Content-Type correto do servidor
const contentType = response.headers['content-type'] || 'application/octet-stream';
console.log(`Download frontend: ${fileName} (${contentType})`);
// Cria o blob com o tipo correto
const blob = new Blob([response.data], { type: contentType });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
// Libera a memória do blob URL
setTimeout(() => window.URL.revokeObjectURL(url), 100);
toast.success('Download iniciado!');
} catch (error) {
console.error('Erro ao fazer download:', error);
toast.error('Erro ao fazer download');
}
};
const deleteOperation = async (operacaoId: string) => {
if (!window.confirm('Deseja realmente deletar esta operação e seus arquivos?')) {
return;
}
try {
const token = getToken();
await axios.delete(`/api/pdf/operation/${operacaoId}`, {
headers: { Authorization: `Bearer ${token}` },
});
toast.success('Operação deletada com sucesso!');
loadHistory();
} catch (error) {
console.error('Erro ao deletar operação:', error);
toast.error('Erro ao deletar operação');
}
};
const clearAllHistory = async () => {
if (!window.confirm('⚠️ ATENÇÃO: Isso irá deletar TODO o histórico e TODOS os arquivos armazenados. Esta ação não pode ser desfeita. Deseja continuar?')) {
return;
}
try {
const token = getToken();
const response = await axios.delete('/api/pdf/history/clear', {
headers: { Authorization: `Bearer ${token}` },
});
toast.success(`Histórico limpo! ${response.data.operacoesRemovidas} operações removidas.`);
setHistory([]);
setResult(null);
} catch (error) {
console.error('Erro ao limpar histórico:', error);
toast.error('Erro ao limpar histórico');
}
};
const cleanupOldFiles = async () => {
if (!window.confirm('Deseja remover operações com mais de 24 horas?')) {
return;
}
try {
const token = getToken();
const response = await axios.delete('/api/pdf/history/cleanup?hours=24', {
headers: { Authorization: `Bearer ${token}` },
});
toast.success(`${response.data.operacoesRemovidas} operações antigas removidas!`);
loadHistory();
} catch (error) {
console.error('Erro ao limpar arquivos antigos:', error);
toast.error('Erro ao limpar arquivos antigos');
}
};
React.useEffect(() => {
loadHistory();
}, []);
const operations = [
{
id: 'compress' as OperationType,
name: 'Comprimir PDF',
icon: ArrowsPointingInIcon,
description: 'Reduza o tamanho mantendo a qualidade',
},
{
id: 'split' as OperationType,
name: 'Dividir PDF',
icon: ScissorsIcon,
description: 'Separe páginas específicas',
},
{
id: 'merge' as OperationType,
name: 'Mesclar PDFs',
icon: ArrowsUpDownIcon,
description: 'Combine múltiplos PDFs',
},
{
id: 'pdfToImages' as OperationType,
name: 'PDF para Imagens',
icon: PhotoIcon,
description: 'Converta páginas em imagens',
},
{
id: 'imagesToPDF' as OperationType,
name: 'Imagens para PDF',
icon: DocumentIcon,
description: 'Crie PDF a partir de imagens',
},
{
id: 'ocr' as OperationType,
name: 'OCR',
icon: DocumentTextIcon,
description: 'Extraia texto de PDFs',
},
{
id: 'info' as OperationType,
name: 'Informações',
icon: InformationCircleIcon,
description: 'Obtenha detalhes do PDF',
},
];
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Manipulação de PDFs</h1>
<p className="mt-2 text-gray-600">
Ferramentas completas para trabalhar com arquivos PDF
</p>
</div>
{/* Seleção de operação */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-8">
{operations.map((op) => (
<button
key={op.id}
onClick={() => {
setSelectedOperation(op.id);
clearFiles();
}}
className={`p-4 rounded-lg border-2 transition-all ${
selectedOperation === op.id
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
>
<op.icon className="h-8 w-8 mx-auto mb-2" />
<div className="text-sm font-medium">{op.name}</div>
</button>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Área principal */}
<div className="lg:col-span-2 space-y-6">
{/* Upload de arquivos */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Arquivos</h2>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${
isDragActive
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
<CloudArrowUpIcon className="h-12 w-12 mx-auto text-gray-400 mb-4" />
{isDragActive ? (
<p className="text-blue-600">Solte os arquivos aqui...</p>
) : (
<div>
<p className="text-gray-600 mb-2">
Arraste e solte arquivos aqui, ou clique para selecionar
</p>
<p className="text-sm text-gray-500">
PDFs e imagens (PNG, JPG, WEBP)
</p>
</div>
)}
</div>
{/* Lista de arquivos */}
{files.length > 0 && (
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<h3 className="font-medium">
Arquivos selecionados ({files.length})
</h3>
<button
onClick={clearFiles}
className="text-sm text-red-600 hover:text-red-700"
>
Limpar todos
</button>
</div>
<div className="space-y-2">
{files.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded"
>
<div className="flex items-center space-x-3">
<DocumentIcon className="h-5 w-5 text-gray-400" />
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-gray-500">
{formatBytes(file.size)}
</p>
</div>
</div>
<button
onClick={() => removeFile(index)}
className="text-red-600 hover:text-red-700"
>
Remover
</button>
</div>
))}
</div>
</div>
)}
</div>
{/* Configurações específicas */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Configurações</h2>
{selectedOperation === 'compress' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Nível de Compressão
</label>
<select
value={compressQuality}
onChange={(e) => setCompressQuality(e.target.value as any)}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="extreme">Extrema (72 DPI) - Máxima redução, visualização na tela</option>
<option value="low">Alta compressão (100 DPI) - Arquivos pequenos</option>
<option value="medium">Equilibrada (150 DPI) - Recomendado</option>
<option value="high">Qualidade (200 DPI) - Para impressão</option>
</select>
<p className="text-xs text-gray-500 mt-2">
💡 Usa Ghostscript, mesmo motor do iLovePDF e SmallPDF
</p>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="enableOCR"
checked={enableOCR}
onChange={(e) => setEnableOCR(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded"
/>
<label htmlFor="enableOCR" className="text-sm font-medium">
Adicionar camada de OCR (tornar PDF pesquisável)
</label>
</div>
<p className="text-xs text-gray-500">
OCR torna o PDF pesquisável. É instantâneo para PDFs digitais.
</p>
</div>
)}
{selectedOperation === 'split' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Tipo de divisão
</label>
<select
value={splitType}
onChange={(e) => setSplitType(e.target.value as any)}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="pages">Páginas específicas</option>
<option value="range">Intervalos</option>
<option value="chunks">Chunks (partes iguais)</option>
</select>
</div>
{splitType === 'pages' && (
<div>
<label className="block text-sm font-medium mb-2">
Páginas (separadas por vírgula)
</label>
<input
type="text"
value={splitPages}
onChange={(e) => setSplitPages(e.target.value)}
placeholder="1,2,3,5,8"
className="w-full border border-gray-300 rounded-lg px-3 py-2"
/>
</div>
)}
{splitType === 'range' && (
<div>
<label className="block text-sm font-medium mb-2">
Intervalos (ex: 1-5,6-10)
</label>
<input
type="text"
value={splitRanges}
onChange={(e) => setSplitRanges(e.target.value)}
placeholder="1-5,6-10,11-15"
className="w-full border border-gray-300 rounded-lg px-3 py-2"
/>
</div>
)}
{splitType === 'chunks' && (
<div>
<label className="block text-sm font-medium mb-2">
Páginas por chunk
</label>
<input
type="number"
value={chunkSize}
onChange={(e) => setChunkSize(parseInt(e.target.value))}
min="1"
className="w-full border border-gray-300 rounded-lg px-3 py-2"
/>
</div>
)}
</div>
)}
{selectedOperation === 'pdfToImages' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Formato</label>
<select
value={imageFormat}
onChange={(e) => setImageFormat(e.target.value as any)}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="png">PNG</option>
<option value="jpg">JPG</option>
<option value="webp">WEBP</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Qualidade ({imageQuality}%)
</label>
<input
type="range"
min="50"
max="100"
value={imageQuality}
onChange={(e) => setImageQuality(parseInt(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
DPI ({imageDensity})
</label>
<input
type="range"
min="150"
max="600"
step="50"
value={imageDensity}
onChange={(e) => setImageDensity(parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
)}
{selectedOperation === 'ocr' && (
<div>
<label className="block text-sm font-medium mb-2">Idioma</label>
<select
value={ocrLanguage}
onChange={(e) => setOcrLanguage(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2"
>
<option value="por">Português</option>
<option value="eng">Inglês</option>
<option value="spa">Espanhol</option>
</select>
</div>
)}
{selectedOperation === 'merge' && (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Selecione 2 ou mais PDFs para mesclar em um único arquivo.
</p>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="enableOCRMerge"
checked={enableOCR}
onChange={(e) => setEnableOCR(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded"
/>
<label htmlFor="enableOCRMerge" className="text-sm font-medium">
Adicionar camada de OCR ao PDF mesclado
</label>
</div>
<p className="text-xs text-gray-500">
⚠️ OCR aumenta significativamente o tempo de processamento
</p>
</div>
)}
{selectedOperation === 'imagesToPDF' && (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Converte múltiplas imagens em um único arquivo PDF.
</p>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="enableOCRImages"
checked={enableOCR}
onChange={(e) => setEnableOCR(e.target.checked)}
className="h-4 w-4 text-blue-600 rounded"
/>
<label htmlFor="enableOCRImages" className="text-sm font-medium">
Adicionar camada de OCR (tornar PDF pesquisável)
</label>
</div>
<p className="text-xs text-gray-500">
⚠️ OCR aumenta significativamente o tempo de processamento
</p>
</div>
)}
<button
onClick={processFiles}
disabled={processing || files.length === 0}
className={`mt-6 w-full py-3 rounded-lg font-medium transition-colors ${
processing || files.length === 0
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{processing ? (
<span className="flex items-center justify-center">
<svg
className="animate-spin h-5 w-5 mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Processando...
</span>
) : (
'Processar'
)}
</button>
</div>
{/* Resultado */}
{result && (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Resultado</h2>
{selectedOperation === 'compress' && result.reduction !== undefined && (
<div className="space-y-2">
<p className="text-green-600 font-medium">
Redução de {result.reduction.toFixed(2)}%
</p>
<p className="text-sm text-gray-600">
Tempo de processamento: {result.processingTime}ms
</p>
</div>
)}
{(result.outputPaths || result.outputPath || result.zipPath) && (
<div className="mt-4">
<p className="font-medium mb-2">Arquivos gerados:</p>
{/* Botão de download ZIP (se disponível) */}
{result.zipPath && (
<button
onClick={() => downloadFile(result.operacaoId, 0)}
className="flex items-center space-x-2 w-full p-3 mb-3 bg-blue-50 hover:bg-blue-100 rounded border-2 border-blue-300"
>
<ArrowDownTrayIcon className="h-6 w-6 text-blue-600" />
<div className="text-left">
<span className="text-sm font-semibold text-blue-900">
📦 Download ZIP (Todos os arquivos)
</span>
<p className="text-xs text-blue-700">
Recomendado para múltiplos arquivos
</p>
</div>
</button>
)}
{/* Arquivos individuais */}
{result.outputPaths?.map((path, index) => {
// Determina o nome do arquivo baseado na operação
let fileName = `Arquivo ${index + 1}`;
if (selectedOperation === 'pdfToImages') {
fileName = `Imagem ${index + 1}`;
} else if (selectedOperation === 'split') {
fileName = `Página ${index + 1}`;
}
return (
<button
key={index}
onClick={() => downloadFile(result.operacaoId, result.zipPath ? index + 1 : index)}
className="flex items-center space-x-2 w-full p-2 hover:bg-gray-50 rounded"
>
<ArrowDownTrayIcon className="h-5 w-5 text-blue-600" />
<span className="text-sm">{fileName}</span>
</button>
);
})}
{result.outputPath && !result.outputPaths && (
<button
onClick={() => downloadFile(result.operacaoId, result.zipPath ? 1 : 0)}
className="flex items-center space-x-2 w-full p-2 hover:bg-gray-50 rounded"
>
<ArrowDownTrayIcon className="h-5 w-5 text-blue-600" />
<span className="text-sm">Download</span>
</button>
)}
</div>
)}
{selectedOperation === 'ocr' && result.text && (
<div className="mt-4">
<p className="font-medium mb-2">Texto extraído:</p>
<div className="bg-gray-50 p-4 rounded max-h-64 overflow-y-auto">
<pre className="text-sm whitespace-pre-wrap">{result.text}</pre>
</div>
{result.confidence && (
<p className="text-sm text-gray-600 mt-2">
Confiança: {result.confidence.toFixed(2)}%
</p>
)}
</div>
)}
</div>
)}
</div>
{/* Histórico */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow p-6 sticky top-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">Histórico</h2>
{history.length > 0 && (
<div className="flex space-x-2">
<button
onClick={cleanupOldFiles}
className="text-xs px-2 py-1 bg-yellow-100 hover:bg-yellow-200 text-yellow-800 rounded transition-colors"
title="Remover operações com mais de 24 horas"
>
🕐 Limpar Antigos
</button>
<button
onClick={clearAllHistory}
className="text-xs px-2 py-1 bg-red-100 hover:bg-red-200 text-red-800 rounded transition-colors"
title="Apagar todo o histórico e arquivos"
>
🗑️ Limpar Tudo
</button>
</div>
)}
</div>
{history.length === 0 ? (
<p className="text-gray-500 text-sm">Nenhuma operação ainda</p>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto">
{history.map((op) => (
<div key={op.id} className="border-b pb-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
{op.tipoOperacao.replace('_', ' ')}
</span>
<div className="flex items-center space-x-2">
<span
className={`text-xs px-2 py-1 rounded ${
op.status === 'CONCLUIDO'
? 'bg-green-100 text-green-800'
: op.status === 'ERRO'
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
}`}
>
{op.status}
</span>
<button
onClick={() => deleteOperation(op.id)}
className="text-red-600 hover:text-red-800 hover:bg-red-50 p-1 rounded transition-colors"
title="Deletar esta operação"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
<div className="flex items-center text-xs text-gray-500 mt-1">
<ClockIcon className="h-3 w-3 mr-1" />
{new Date(op.criadoEm).toLocaleString('pt-BR')}
</div>
{op.tempoProcessamento && (
<p className="text-xs text-gray-500 mt-1">
{op.tempoProcessamento}ms
</p>
)}
{op.status === 'CONCLUIDO' && op.arquivosResultado && op.arquivosResultado.length > 0 && (
<div className="mt-2 space-y-1">
{op.arquivosResultado.map((arquivo: string, index: number) => (
<button
key={index}
onClick={() => downloadFile(op.id, index)}
className="flex items-center space-x-1 w-full text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 p-1 rounded"
>
<ArrowDownTrayIcon className="h-3 w-3" />
<span>Arquivo {index + 1}</span>
</button>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};
+1 -2
View File
@@ -1,8 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import api from "../../services/api";
import { Usuario } from "../../types/usuario"; import { Usuario } from "../../types/usuario";
import api from "../../services/api";
import { UF_BRASIL } from "../../utils/constants/estados"; import { UF_BRASIL } from "../../utils/constants/estados";
export function PerfilPage() { export function PerfilPage() {
+3 -4
View File
@@ -1,12 +1,11 @@
import { useState, lazy, Suspense, useEffect } from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import { useState, lazy, Suspense, useEffect } from "react"; import { formatCurrency, formatDate } from "../../utils/format";
import { useProcessoContext } from "../../context/ProcessoContext";
import { useCustasProcessuais } from "../../hooks/useCustasProcessuais"; import { useCustasProcessuais } from "../../hooks/useCustasProcessuais";
import { useHonorarios } from "../../hooks/useHonorarios"; import { useHonorarios } from "../../hooks/useHonorarios";
import { useProcessoContext } from "../../context/ProcessoContext";
import api from "../../services/api"; // Adicionar esta importação import api from "../../services/api"; // Adicionar esta importação
import { formatCurrency, formatDate } from "../../utils/format";
// Corrigir importações lazy // Corrigir importações lazy
const DialogCustaProcessual = lazy(() => const DialogCustaProcessual = lazy(() =>
+1 -2
View File
@@ -1,8 +1,7 @@
import { useProcessoContext } from "../../context/ProcessoContext";
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { useProcessoContext } from "../../context/ProcessoContext";
export function AbaInformacoes() { export function AbaInformacoes() {
const { processo } = useProcessoContext(); const { processo } = useProcessoContext();
@@ -1,9 +1,8 @@
import { ClockIcon, DocumentIcon } from "@heroicons/react/24/outline";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useProcessoContext } from "../../context/ProcessoContext"; import { useProcessoContext } from "../../context/ProcessoContext";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes"; import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { ClockIcon, DocumentIcon } from "@heroicons/react/24/outline";
export default function AbaMovimentacoes() { export default function AbaMovimentacoes() {
const { processo } = useProcessoContext(); const { processo } = useProcessoContext();
+1 -2
View File
@@ -1,7 +1,6 @@
import { Dialog } from "@headlessui/react";
import React from "react"; import React from "react";
import { Dialog } from "@headlessui/react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { FormSelect, FormInput } from "../forms"; import { FormSelect, FormInput } from "../forms";
const TIPOS_AUDIENCIA = [ const TIPOS_AUDIENCIA = [
@@ -6,12 +6,12 @@ import {
Box, Box,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { ptBR } from "date-fns/locale";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
interface DialogCustaProcessualProps { interface DialogCustaProcessualProps {
open: boolean; open: boolean;
+5 -6
View File
@@ -9,13 +9,12 @@ import {
Select, Select,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
import { IHonorario, IHonorarioCreate } from "../types/financeiro"; import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps { interface DialogHonorarioProps {
@@ -1,6 +1,5 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes"; import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
interface MovimentacoesTimelineProps { interface MovimentacoesTimelineProps {
@@ -1,9 +1,8 @@
import { Dialog } from "@headlessui/react";
import React, { useState } from "react"; import React, { useState } from "react";
import { useForm } from "react-hook-form"; import { Dialog } from "@headlessui/react";
import { useClientes } from "../../hooks/useClientes"; import { useClientes } from "../../hooks/useClientes";
import { FormSelect, FormInput } from "../forms"; import { FormSelect, FormInput } from "../forms";
import { useForm } from "react-hook-form";
interface ParteProcessoModalProps { interface ParteProcessoModalProps {
isOpen: boolean; isOpen: boolean;
+16 -22
View File
@@ -1,25 +1,24 @@
import React from "react"; import React from 'react';
import { Processo } from '../../types/entities';
import { Processo } from "../../types/entities";
interface ProcessoCardProps { interface ProcessoCardProps {
processo: Processo; processo: Processo;
} }
const resultadoLabel = { const resultadoLabel = {
NAO_JULGADO: "Não Julgado", NAO_JULGADO: 'Não Julgado',
PROCEDENTE: "Procedente", PROCEDENTE: 'Procedente',
PARCIALMENTE_PROCEDENTE: "Parcialmente Procedente", PARCIALMENTE_PROCEDENTE: 'Parcialmente Procedente',
IMPROCEDENTE: "Improcedente", IMPROCEDENTE: 'Improcedente',
SEM_ANALISE_MERITO: "Sem Análise de Mérito", SEM_ANALISE_MERITO: 'Sem Análise de Mérito'
}; };
const resultadoColor = { const resultadoColor = {
NAO_JULGADO: "bg-gray-100 text-gray-800", NAO_JULGADO: 'bg-gray-100 text-gray-800',
PROCEDENTE: "bg-green-100 text-green-800", PROCEDENTE: 'bg-green-100 text-green-800',
PARCIALMENTE_PROCEDENTE: "bg-yellow-100 text-yellow-800", PARCIALMENTE_PROCEDENTE: 'bg-yellow-100 text-yellow-800',
IMPROCEDENTE: "bg-red-100 text-red-800", IMPROCEDENTE: 'bg-red-100 text-red-800',
SEM_ANALISE_MERITO: "bg-gray-100 text-gray-800", SEM_ANALISE_MERITO: 'bg-gray-100 text-gray-800'
}; };
export const ProcessoCard: React.FC<ProcessoCardProps> = ({ processo }) => { export const ProcessoCard: React.FC<ProcessoCardProps> = ({ processo }) => {
@@ -41,21 +40,16 @@ export const ProcessoCard: React.FC<ProcessoCardProps> = ({ processo }) => {
{processo.status} {processo.status}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{processo.valorCausa?.toLocaleString("pt-BR", { {processo.valorCausa?.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
style: "currency",
currency: "BRL",
})}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(processo.dataCriacao).toLocaleDateString("pt-BR")} {new Date(processo.dataCriacao).toLocaleDateString('pt-BR')}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(processo.dataAtualizacao).toLocaleDateString("pt-BR")} {new Date(processo.dataAtualizacao).toLocaleDateString('pt-BR')}
</p> </p>
<div className="mt-2"> <div className="mt-2">
<span <span className={`px-2 py-1 text-xs rounded-full ${resultadoColor[processo.resultado]}`}>
className={`px-2 py-1 text-xs rounded-full ${resultadoColor[processo.resultado]}`}
>
{resultadoLabel[processo.resultado]} {resultadoLabel[processo.resultado]}
</span> </span>
</div> </div>
+17 -21
View File
@@ -1,6 +1,10 @@
import React, { useState, useEffect, lazy, Suspense } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useProcessos } from "../../hooks/useProcessos";
import api from "../../services/api"; // Adicionar esta importação
import { Tab, Dialog } from "@headlessui/react"; import { Tab, Dialog } from "@headlessui/react";
import { TrashIcon } from "@heroicons/react/24/outline";
import { import {
TrashIcon,
ArrowLeftIcon, ArrowLeftIcon,
ClockIcon, ClockIcon,
DocumentTextIcon, DocumentTextIcon,
@@ -11,20 +15,14 @@ import {
ExclamationTriangleIcon, ExclamationTriangleIcon,
PlusIcon, PlusIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import React, { useState, useEffect, lazy, Suspense } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ProcessoProvider } from "../../context/ProcessoContext";
import { useAudiencias } from "../../hooks/useAudiencias";
import { usePartesProcesso } from "../../hooks/usePartesProcesso";
import { useProcessos } from "../../hooks/useProcessos";
import api from "../../services/api"; // Adicionar esta importação
import { formatNumeroProcesso, formatCPF } from "../../utils/formatters";
import AbaFinanceiro from "./AbaFinanceiro";
import { AudienciaModal } from "./AudienciaModal";
import { ParteProcessoModal } from "./ParteProcessoModal";
import { ProcessoModal } from "./ProcessoModal.tsx"; import { ProcessoModal } from "./ProcessoModal.tsx";
import { formatNumeroProcesso, formatCPF } from "../../utils/formatters";
import { ParteProcessoModal } from "./ParteProcessoModal";
import { usePartesProcesso } from "../../hooks/usePartesProcesso";
import { useAudiencias } from "../../hooks/useAudiencias";
import { AudienciaModal } from "./AudienciaModal";
import AbaFinanceiro from "./AbaFinanceiro";
import { ProcessoProvider } from "../../context/ProcessoContext";
// Adicionar lazy import para AbaMovimentacoes // Adicionar lazy import para AbaMovimentacoes
const AbaMovimentacoes = lazy(() => import("./AbaMovimentacoes")); const AbaMovimentacoes = lazy(() => import("./AbaMovimentacoes"));
@@ -194,27 +192,25 @@ export const ProcessoDetalhes: React.FC = () => {
{/* Adicionar o botão de resultado aqui */} {/* Adicionar o botão de resultado aqui */}
<select <select
value={processo.resultado || "NAO_JULGADO"} value={processo.resultado || 'NAO_JULGADO'}
onChange={async (e) => { onChange={async (e) => {
try { try {
await api.patch(`/processos/${processo.id}/resultado`, { await api.patch(`/processos/${processo.id}/resultado`, {
resultado: e.target.value, resultado: e.target.value
}); });
// Recarregar dados após atualização // Recarregar dados após atualização
const updated = await getProcesso(processo.id); const updated = await getProcesso(processo.id);
setProcesso(updated); setProcesso(updated);
} catch (error) { } catch (error) {
console.error("Erro ao atualizar resultado:", error); console.error('Erro ao atualizar resultado:', error);
alert("Erro ao atualizar resultado do processo"); alert('Erro ao atualizar resultado do processo');
} }
}} }}
className="rounded-lg border-gray-300 text-sm" className="rounded-lg border-gray-300 text-sm"
> >
<option value="NAO_JULGADO">Não Julgado</option> <option value="NAO_JULGADO">Não Julgado</option>
<option value="PROCEDENTE">Procedente</option> <option value="PROCEDENTE">Procedente</option>
<option value="PARCIALMENTE_PROCEDENTE"> <option value="PARCIALMENTE_PROCEDENTE">Parcialmente Procedente</option>
Parcialmente Procedente
</option>
<option value="IMPROCEDENTE">Improcedente</option> <option value="IMPROCEDENTE">Improcedente</option>
<option value="SEM_ANALISE_MERITO">Sem Análise de Mérito</option> <option value="SEM_ANALISE_MERITO">Sem Análise de Mérito</option>
</select> </select>
+18 -22
View File
@@ -1,7 +1,6 @@
import React from "react"; import React from 'react';
import { useForm } from "react-hook-form"; import { useForm } from 'react-hook-form';
import { Processo } from '../../types/entities';
import { Processo } from "../../types/entities";
interface ProcessoFormProps { interface ProcessoFormProps {
onSubmit: (data: Partial<Processo>) => void; onSubmit: (data: Partial<Processo>) => void;
@@ -9,17 +8,14 @@ interface ProcessoFormProps {
} }
const resultadoOptions = [ const resultadoOptions = [
{ value: "NAO_JULGADO", label: "Não Julgado" }, { value: 'NAO_JULGADO', label: 'Não Julgado' },
{ value: "PROCEDENTE", label: "Procedente" }, { value: 'PROCEDENTE', label: 'Procedente' },
{ value: "PARCIALMENTE_PROCEDENTE", label: "Parcialmente Procedente" }, { value: 'PARCIALMENTE_PROCEDENTE', label: 'Parcialmente Procedente' },
{ value: "IMPROCEDENTE", label: "Improcedente" }, { value: 'IMPROCEDENTE', label: 'Improcedente' },
{ value: "SEM_ANALISE_MERITO", label: "Sem Análise de Mérito" }, { value: 'SEM_ANALISE_MERITO', label: 'Sem Análise de Mérito' }
]; ];
export const ProcessoForm: React.FC<ProcessoFormProps> = ({ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialData }) => {
onSubmit,
initialData,
}) => {
const { register, handleSubmit, reset } = useForm<Partial<Processo>>({ const { register, handleSubmit, reset } = useForm<Partial<Processo>>({
defaultValues: initialData, defaultValues: initialData,
}); });
@@ -32,7 +28,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Número do Processo Número do Processo
</label> </label>
<input <input
{...register("numero")} {...register('numero')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/> />
</div> </div>
@@ -42,7 +38,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Natureza Natureza
</label> </label>
<select <select
{...register("natureza")} {...register('natureza')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
> >
<option value="CIVEL">Cível</option> <option value="CIVEL">Cível</option>
@@ -62,7 +58,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Status Status
</label> </label>
<select <select
{...register("status")} {...register('status')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
> >
<option value="INICIAL">Inicial</option> <option value="INICIAL">Inicial</option>
@@ -79,10 +75,10 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Resultado do Julgamento Resultado do Julgamento
</label> </label>
<select <select
{...register("resultado")} {...register('resultado')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
> >
{resultadoOptions.map((option) => ( {resultadoOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.label}
</option> </option>
@@ -97,7 +93,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Cliente Cliente
</label> </label>
<input <input
{...register("clienteId")} {...register('clienteId')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/> />
</div> </div>
@@ -108,7 +104,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
</label> </label>
<input <input
type="number" type="number"
{...register("valorCausa")} {...register('valorCausa')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/> />
</div> </div>
@@ -120,7 +116,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Órgão Julgador Órgão Julgador
</label> </label>
<input <input
{...register("orgaoJulgador")} {...register('orgaoJulgador')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/> />
</div> </div>
@@ -130,7 +126,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({
Nome da Contraparte Nome da Contraparte
</label> </label>
<input <input
{...register("nomeContraparte")} {...register('nomeContraparte')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700" className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/> />
</div> </div>
@@ -1,6 +1,5 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { DatajudProcesso } from "../../types/datajud"; import { DatajudProcesso } from "../../types/datajud";
interface ProcessoInfoProps { interface ProcessoInfoProps {
+8 -10
View File
@@ -1,22 +1,20 @@
import React, { useState, useEffect } from "react";
import { Dialog, Transition, Combobox } from "@headlessui/react"; import { Dialog, Transition, Combobox } from "@headlessui/react";
import { import {
XMarkIcon, XMarkIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
CheckIcon, CheckIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import axios from "axios";
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useClientes } from "../../hooks/useClientes";
import { useDatajud } from "../../hooks/useDatajud";
import { useProcessos } from "../../hooks/useProcessos";
import { DatajudProcesso } from "../../types/datajud";
import { Processo, NaturezaProcesso, TipoParte } from "../../types/entities"; import { Processo, NaturezaProcesso, TipoParte } from "../../types/entities";
import { UF_BRASIL, TRIBUNAIS } from "../../utils/constants/tribunais"; import { useProcessos } from "../../hooks/useProcessos";
import { useClientes } from "../../hooks/useClientes";
import { FormInput, FormSelect, FormCheckbox } from "../forms"; import { FormInput, FormSelect, FormCheckbox } from "../forms";
import axios from "axios";
import { UF_BRASIL, TRIBUNAIS } from "../../utils/constants/tribunais";
import { useDatajud } from "../../hooks/useDatajud";
import { ProcessoInfo } from "./ProcessoInfo"; // Adicionar import import { ProcessoInfo } from "./ProcessoInfo"; // Adicionar import
import { DatajudProcesso } from "../../types/datajud";
interface ProcessoModalProps { interface ProcessoModalProps {
isOpen: boolean; isOpen: boolean;
@@ -142,7 +140,7 @@ export const ProcessoModal: React.FC<ProcessoModalProps> = ({
if (processo) { if (processo) {
await updateProcesso.mutateAsync({ await updateProcesso.mutateAsync({
id: processo.id, id: processo.id,
data: processData, data: processData
}); });
} else { } else {
await createProcesso.mutateAsync(processData); await createProcesso.mutateAsync(processData);
+42 -79
View File
@@ -1,51 +1,43 @@
import { format } from "date-fns"; import React from 'react';
import { ptBR } from "date-fns/locale"; import { useQuery } from 'react-query';
import React from "react"; import { useParams } from 'react-router-dom';
import { useQuery } from "react-query"; import api from '../../services/api';
import { useParams } from "react-router-dom"; import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import api from "../../services/api"; import { Processo } from '../../types/entities';
import { Processo } from "../../types/entities";
const resultadoOptions = [ const resultadoOptions = [
{ value: "NAO_JULGADO", label: "Não Julgado" }, { value: 'NAO_JULGADO', label: 'Não Julgado' },
{ value: "PROCEDENTE", label: "Procedente" }, { value: 'PROCEDENTE', label: 'Procedente' },
{ value: "PARCIALMENTE_PROCEDENTE", label: "Parcialmente Procedente" }, { value: 'PARCIALMENTE_PROCEDENTE', label: 'Parcialmente Procedente' },
{ value: "IMPROCEDENTE", label: "Improcedente" }, { value: 'IMPROCEDENTE', label: 'Improcedente' },
{ value: "SEM_ANALISE_MERITO", label: "Sem Análise de Mérito" }, { value: 'SEM_ANALISE_MERITO', label: 'Sem Análise de Mérito' }
]; ];
const getResultadoLabel = (valor: string) => { const getResultadoLabel = (valor: string) => {
return resultadoOptions.find((opt) => opt.value === valor)?.label || valor; return resultadoOptions.find(opt => opt.value === valor)?.label || valor;
}; };
const getResultadoColor = (resultado: string) => { const getResultadoColor = (resultado: string) => {
switch (resultado) { switch (resultado) {
case "PROCEDENTE": case 'PROCEDENTE': return 'bg-green-100 text-green-800';
return "bg-green-100 text-green-800"; case 'PARCIALMENTE_PROCEDENTE': return 'bg-yellow-100 text-yellow-800';
case "PARCIALMENTE_PROCEDENTE": case 'IMPROCEDENTE': return 'bg-red-100 text-red-800';
return "bg-yellow-100 text-yellow-800"; case 'SEM_ANALISE_MERITO': return 'bg-gray-100 text-gray-800';
case "IMPROCEDENTE": default: return 'bg-blue-100 text-blue-800';
return "bg-red-100 text-red-800";
case "SEM_ANALISE_MERITO":
return "bg-gray-100 text-gray-800";
default:
return "bg-blue-100 text-blue-800";
} }
}; };
export const ProcessoView: React.FC = () => { export const ProcessoView: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { const { data: processo, refetch, isLoading, isError } = useQuery<Processo>(
data: processo, ['processo', id],
refetch, async () => {
isLoading,
isError,
} = useQuery<Processo>(["processo", id], async () => {
const response = await api.get(`/processos/${id}`); // remover /api const response = await api.get(`/processos/${id}`); // remover /api
return response.data; return response.data;
}); }
);
if (isLoading) { if (isLoading) {
return <div>Carregando...</div>; return <div>Carregando...</div>;
@@ -60,9 +52,7 @@ export const ProcessoView: React.FC = () => {
{/* Status e Resultado do Processo */} {/* Status e Resultado do Processo */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white"> <h3 className="text-lg font-medium text-gray-900 dark:text-white">Status do Processo</h3>
Status do Processo
</h3>
{/* Botão de Julgamento */} {/* Botão de Julgamento */}
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@@ -72,17 +62,17 @@ export const ProcessoView: React.FC = () => {
onChange={async (e) => { onChange={async (e) => {
try { try {
await api.patch(`/processos/${processo.id}/resultado`, { await api.patch(`/processos/${processo.id}/resultado`, {
resultado: e.target.value, resultado: e.target.value
}); });
refetch(); refetch();
} catch (error) { } catch (error) {
console.error("Erro ao atualizar resultado:", error); console.error('Erro ao atualizar resultado:', error);
alert("Erro ao atualizar resultado do processo"); alert('Erro ao atualizar resultado do processo');
} }
}} }}
className="rounded-lg border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600" className="rounded-lg border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600"
> >
{resultadoOptions.map((option) => ( {resultadoOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.label}
</option> </option>
@@ -100,12 +90,8 @@ export const ProcessoView: React.FC = () => {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-500"> <span className="text-sm font-medium text-gray-500">Resultado:</span>
Resultado: <span className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}>
</span>
<span
className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}
>
{getResultadoLabel(processo.resultado)} {getResultadoLabel(processo.resultado)}
</span> </span>
</div> </div>
@@ -120,52 +106,31 @@ export const ProcessoView: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<h3 className="text-lg font-medium">Cliente</h3> <h3 className="text-lg font-medium">Cliente</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{processo.cliente.nome}</p>
{processo.cliente.nome}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Contraparte</h3> <h3 className="text-lg font-medium">Contraparte</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{processo.nomeContraparte}</p>
{processo.nomeContraparte}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Natureza</h3> <h3 className="text-lg font-medium">Natureza</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{processo.natureza}</p>
{processo.natureza}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Status</h3> <h3 className="text-lg font-medium">Status</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{processo.status}</p>
{processo.status}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Valor da Causa</h3> <h3 className="text-lg font-medium">Valor da Causa</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{processo.valorCausa?.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</p>
{processo.valorCausa?.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
})}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Data de Criação</h3> <h3 className="text-lg font-medium">Data de Criação</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{format(new Date(processo.dataCriacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
{format(new Date(processo.dataCriacao), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium">Última Atualização</h3> <h3 className="text-lg font-medium">Última Atualização</h3>
<p className="text-gray-700 dark:text-gray-300"> <p className="text-gray-700 dark:text-gray-300">{format(new Date(processo.dataAtualizacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
{format(new Date(processo.dataAtualizacao), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -173,9 +138,7 @@ export const ProcessoView: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4">Resultado</h3> <h3 className="text-lg font-medium mb-4">Resultado</h3>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span <span className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}>
className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}
>
{getResultadoLabel(processo.resultado)} {getResultadoLabel(processo.resultado)}
</span> </span>
<select <select
@@ -183,17 +146,17 @@ export const ProcessoView: React.FC = () => {
onChange={async (e) => { onChange={async (e) => {
try { try {
await api.patch(`/processos/${processo.id}/resultado`, { await api.patch(`/processos/${processo.id}/resultado`, {
resultado: e.target.value, resultado: e.target.value
}); });
refetch(); // Atualizar dados do processo refetch(); // Atualizar dados do processo
} catch (error) { } catch (error) {
console.error("Erro ao atualizar resultado:", error); console.error('Erro ao atualizar resultado:', error);
alert("Erro ao atualizar resultado do processo"); alert('Erro ao atualizar resultado do processo');
} }
}} }}
className="rounded-lg border-gray-300 text-sm" className="rounded-lg border-gray-300 text-sm"
> >
{resultadoOptions.map((option) => ( {resultadoOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.label}
</option> </option>
+15 -24
View File
@@ -1,10 +1,9 @@
import { DocumentTextIcon, ClockIcon } from "@heroicons/react/24/outline";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useProcessos } from "../../hooks/useProcessos";
import { Processo } from "../../types/entities"; import { Processo } from "../../types/entities";
import { useProcessos } from "../../hooks/useProcessos";
import { formatNumeroProcesso } from "../../utils/formatters"; import { formatNumeroProcesso } from "../../utils/formatters";
import { DocumentTextIcon, ClockIcon } from "@heroicons/react/24/outline";
interface ProcessosListProps { interface ProcessosListProps {
searchTerm: string; searchTerm: string;
@@ -12,26 +11,23 @@ interface ProcessosListProps {
} }
const resultadoLabel = { const resultadoLabel = {
NAO_JULGADO: "Não Julgado", NAO_JULGADO: 'Não Julgado',
PROCEDENTE: "Procedente", PROCEDENTE: 'Procedente',
PARCIALMENTE_PROCEDENTE: "Parcialmente Procedente", PARCIALMENTE_PROCEDENTE: 'Parcialmente Procedente',
IMPROCEDENTE: "Improcedente", IMPROCEDENTE: 'Improcedente',
SEM_ANALISE_MERITO: "Sem Análise de Mérito", SEM_ANALISE_MERITO: 'Sem Análise de Mérito'
}; };
const resultadoColor = { const resultadoColor = {
NAO_JULGADO: "bg-gray-100 text-gray-800", NAO_JULGADO: 'bg-gray-100 text-gray-800',
PROCEDENTE: "bg-green-100 text-green-800", PROCEDENTE: 'bg-green-100 text-green-800',
PARCIALMENTE_PROCEDENTE: "bg-yellow-100 text-yellow-800", PARCIALMENTE_PROCEDENTE: 'bg-yellow-100 text-yellow-800',
IMPROCEDENTE: "bg-red-100 text-red-800", IMPROCEDENTE: 'bg-red-100 text-red-800',
SEM_ANALISE_MERITO: "bg-gray-100 text-gray-800", SEM_ANALISE_MERITO: 'bg-gray-100 text-gray-800'
}; };
const getResultadoColor = (resultado: string) => { const getResultadoColor = (resultado: string) => {
return ( return resultadoColor[resultado as keyof typeof resultadoColor] || 'bg-gray-100 text-gray-800';
resultadoColor[resultado as keyof typeof resultadoColor] ||
"bg-gray-100 text-gray-800"
);
}; };
export const ProcessosList: React.FC<ProcessosListProps> = ({ export const ProcessosList: React.FC<ProcessosListProps> = ({
@@ -50,10 +46,7 @@ export const ProcessosList: React.FC<ProcessosListProps> = ({
); );
const getResultadoDisplay = (processo: Processo) => { const getResultadoDisplay = (processo: Processo) => {
if ( if (!processo.resultado || !resultadoLabel[processo.resultado as keyof typeof resultadoLabel]) {
!processo.resultado ||
!resultadoLabel[processo.resultado as keyof typeof resultadoLabel]
) {
return ( return (
<span className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800"> <span className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">
Não Julgado Não Julgado
@@ -62,9 +55,7 @@ export const ProcessosList: React.FC<ProcessosListProps> = ({
} }
return ( return (
<span <span className={`px-2 py-1 text-xs rounded-full ${getResultadoColor(processo.resultado)}`}>
className={`px-2 py-1 text-xs rounded-full ${getResultadoColor(processo.resultado)}`}
>
{resultadoLabel[processo.resultado as keyof typeof resultadoLabel]} {resultadoLabel[processo.resultado as keyof typeof resultadoLabel]}
</span> </span>
); );
+6 -6
View File
@@ -1,10 +1,8 @@
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import React, { useState } from "react"; import React, { useState } from "react";
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Processo } from "../../types/entities";
import { ProcessoModal } from "./ProcessoModal";
import { ProcessosList } from "./ProcessosList"; import { ProcessosList } from "./ProcessosList";
import { ProcessoModal } from "./ProcessoModal";
import { Processo } from "../../types/entities";
export const ProcessosPage: React.FC = () => { export const ProcessosPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -45,7 +43,9 @@ export const ProcessosPage: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900"> <thead className="bg-gray-50 dark:bg-gray-900">
<tr>{/* ...headers... */}</tr> <tr>
{/* ...headers... */}
</tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{/* ...rows... */} {/* ...rows... */}
@@ -6,12 +6,12 @@ import {
Box, Box,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
interface DialogCustaProcessualProps { interface DialogCustaProcessualProps {
open: boolean; open: boolean;
@@ -9,13 +9,12 @@ import {
Select, Select,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
import { IHonorario, IHonorarioCreate } from "../types/financeiro"; import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps { interface DialogHonorarioProps {
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more