Compare commits

23 Commits
Author SHA1 Message Date
rodrigo 479bc5d2d6 fix atendimentos.ts
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 23:30:53 -03:00
rodrigo ea0481e09e Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:41:26 -03:00
rodrigo f9d4f381f0 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:35:41 -03:00
rodrigo 76aa2eb9eb Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:31:02 -03:00
rodrigo bdf3729b32 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:27:49 -03:00
rodrigo ff59d8f090 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:22:09 -03:00
rodrigo 1e27604668 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:11:44 -03:00
rodrigo 7c49890f88 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:02:05 -03:00
rodrigo 01a7c1ff39 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 22:00:24 -03:00
rodrigo 04c4838337 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:58:17 -03:00
rodrigo 6225a212f4 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:53:15 -03:00
rodrigo 7a7e5a0ca1 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:49:12 -03:00
rodrigo f993d9ebfc Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:41:09 -03:00
rodrigo 4143b67b63 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:30:44 -03:00
rodrigo 9055d9d83f Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:28:05 -03:00
rodrigo 5cb8919941 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:22:06 -03:00
rodrigo 605b11b2db Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:19:17 -03:00
rodrigo f6d0db97ea Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:14:57 -03:00
rodrigo 95de429563 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:12:10 -03:00
rodrigo f0a690871d Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:08:07 -03:00
rodrigo 98e902f923 Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 21:03:23 -03:00
rodrigo 5acec6b71e Lint errors
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 20:32:39 -03:00
rodrigo 9742d62ba2 Add CEP autocompletion by ViaCEP/ECT
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-12 20:02:58 -03:00
195 changed files with 9614 additions and 5217 deletions

No files matched your search

+100 -19
View File
@@ -1,29 +1,110 @@
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [ extends: [
'plugin:react/recommended', "eslint:recommended",
'plugin:@typescript-eslint/recommended', "plugin:react/recommended",
'plugin:react-hooks/recommended', "plugin:react-hooks/recommended",
'plugin:prettier/recommended' "plugin:@typescript-eslint/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,
}
}, },
plugins: ['react', '@typescript-eslint', 'react-hooks'], ecmaVersion: 2021,
rules: { sourceType: "module",
'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",
},
},
],
}; };
+1685 -388
View File
File diff suppressed because it is too large. Load diff
+16 -11
View File
@@ -15,11 +15,12 @@
"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 .ts,.tsx", "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix", "lint:fix": "eslint . --ext .js,.jsx,.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"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
@@ -36,7 +37,7 @@
"@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.6.5", "axios": "^1.8.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"bullmq": "^4.12.0", "bullmq": "^4.12.0",
@@ -44,6 +45,7 @@
"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",
@@ -62,10 +64,10 @@
"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": {
@@ -80,15 +82,18 @@
"@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.28.0", "@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.29.1",
"@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": "^9.23.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.5", "eslint-import-resolver-typescript": "^4.3.2",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-import": "^2.31.0",
"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",
+2 -2
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
'tailwindcss/nesting': {}, "tailwindcss/nesting": {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };
@@ -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);
}) })
+126 -119
View File
@@ -1,216 +1,223 @@
tinymce.addI18n("pt_BR", {
tinymce.addI18n('pt_BR',{ Cut: "Recortar",
"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.": "Seu navegador n\u00e3o suporta acesso direto \u00e0 \u00e1rea de transfer\u00eancia. Por favor use os atalhos Ctrl+X - C - V do teclado", "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",
"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.": "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.", "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.":
"Underline": "Sublinhar", "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.",
"Cancel": "Cancelar", Underline: "Sublinhar",
"Justify": "Justificar", Cancel: "Cancelar",
"Inline": "Em linha", Justify: "Justificar",
"Copy": "Copiar", Inline: "Em linha",
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 deve come\u00e7ar com uma letra, seguido apenas por letras, n\u00fameros, tra\u00e7os, pontos, dois pontos ou sublinhados.", "Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.":
"Name": "Nome", "Id deve come\u00e7ar com uma letra, seguido apenas por letras, n\u00fameros, tra\u00e7os, pontos, dois pontos ou sublinhados.",
"Anchor": "\u00c2ncora", Name: "Nome",
"Id": "Id", Anchor: "\u00c2ncora",
"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?", 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?",
"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?": "A URL que voc\u00ea informou parece ser um link externo. Deseja incluir o prefixo http:\/\/?", "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:\/\/?",
"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:": "Insira o c\u00f3digo de incorpora\u00e7\u00e3o abaixo:", "Paste your embed code below:":
"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.": "N\u00e3o foi poss\u00edvel encontrar o termo especificado", "Could not find the specified string.":
"Replace": "Substituir", "N\u00e3o foi poss\u00edvel encontrar o termo especificado",
"Next": "Pr\u00f3ximo", Replace: "Substituir",
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",
@@ -220,13 +227,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": "\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", "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help":
"Tools": "Ferramentas", "\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",
"View": "Visualizar", Tools: "Ferramentas",
"Table": "Tabela", View: "Visualizar",
"Format": "Formatar" Table: "Tabela",
Format: "Formatar",
}); });
+4 -5
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,7 +44,6 @@ 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 {
+130 -64
View File
@@ -1,66 +1,87 @@
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 { Dashboard } from "./components/Dashboard"; import { ThemeProvider } from "./contexts/ThemeContext";
import { MainLayout } from "./components/layouts/MainLayout";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/AuthContext";
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 { PerfilPage } from "./components/perfil/PerfilPage";
import { AtendimentosPage } from "./components/atendimentos/AtendimentosPage";
import { AtendimentoDetalhes } from "./components/atendimentos/AtendimentoDetalhes";
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 { TarefasPage } from "./components/tarefas/TarefasPage";
import { DocumentosPage } from './components/DocumentosPage'; import { DocumentosPage } from "./components/DocumentosPage";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import { ToastContainer } from "react-toastify";
import { GerenciarTemplates } from './components/documentos/GerenciarTemplates';
import { ConfiguracaoDocumento } from './components/documentos/ConfiguracaoDocumento'; import { AdminRoute } from "./components/AdminRoute";
import { ProcessoAdministrativoList } from './components/processosAdministrativos/ProcessoAdministrativoList'; import { AtendimentoDetalhes } from "./components/atendimentos/AtendimentoDetalhes";
import { ProcessoAdminDetalhes } from './components/processosAdministrativos/ProcessoAdminDetalhes'; import { AtendimentosPage } from "./components/atendimentos/AtendimentosPage";
import { ProtectedRoute } from './components/ProtectedRoute';
import { ProvaUpload } from './components/provas/ProvaUpload'; import "react-toastify/dist/ReactToastify.css";
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 { NovoCalculo } from './components/calculos/NovoCalculo'; import { DetalhesCalculo } from "./components/calculos/DetalhesCalculo";
import { DetalhesCalculo } from './components/calculos/DetalhesCalculo'; import { NovoCalculo } from "./components/calculos/NovoCalculo";
import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia'; import { NovoPensaoAlimenticia } from "./components/calculos/NovoPensaoAlimenticia";
import { ClienteDetalhes } from "./components/clientes/ClienteDetalhes";
import { ClientesPage } from "./components/clientes/ClientesPage";
import { ConfiguracaoDocumento } from "./components/documentos/ConfiguracaoDocumento";
import { GerenciarTemplates } from "./components/documentos/GerenciarTemplates";
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 path="/" element={<Navigate to="/dashboard" />} errorElement={<ErrorBoundary />} /> <Route
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 path="/public/verificar/:hash" element={<PublicVerificarProva />} errorElement={<ErrorBoundary />} /> <Route
<Route path="/public/provas/upload" element={<ClientProvaUpload />} errorElement={<ErrorBoundary />} /> path="/public/verificar/:hash"
<Route path="/public/provas/status/:id" element={<PublicProvaStatus />} errorElement={<ErrorBoundary />} /> element={<PublicVerificarProva />}
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 />}>
@@ -68,9 +89,18 @@ 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 path="documentos/configuracoes" element={<ConfiguracaoDocumento />} /> <Route
<Route path="processos/:processoId/documentos" element={<DocumentosPage />} /> path="documentos/configuracoes"
<Route path="atendimentos/:atendimentoId/documentos" element={<DocumentosPage />} /> element={<ConfiguracaoDocumento />}
/>
<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 />} />
@@ -79,56 +109,92 @@ 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 path="/processos-administrativos" element={<ProtectedRoute roles={['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE']}><ProcessoAdministrativoList /></ProtectedRoute>} /> <Route
<Route path="/processos-administrativos/:id" element={<ProtectedRoute roles={['ADVOGADO_ADMIN', 'ADVOGADO', 'ASSISTENTE']}><ProcessoAdminDetalhes /></ProtectedRoute>} /> path="/processos-administrativos"
element={
<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 path="/provas/admin" element={ <Route
<ProtectedRoute roles={['ADVOGADO_ADMIN']}> path="/provas/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 path="/calculos" element={ <Route
path="/calculos"
element={
<ProtectedRoute> <ProtectedRoute>
<CalculosList /> <CalculosList />
</ProtectedRoute> </ProtectedRoute>
} /> }
/>
<Route path="/calculos/novo" element={ <Route
path="/calculos/novo"
element={
<ProtectedRoute> <ProtectedRoute>
<NovoCalculo /> <NovoCalculo />
</ProtectedRoute> </ProtectedRoute>
} /> }
/>
<Route path="/calculos/:id" element={ <Route
path="/calculos/:id"
element={
<ProtectedRoute> <ProtectedRoute>
<DetalhesCalculo /> <DetalhesCalculo />
</ProtectedRoute> </ProtectedRoute>
} /> }
/>
{/* Rotas para Pensão Alimentícia */} {/* Rotas para Pensão Alimentícia */}
<Route path="/pensao-alimenticia/nova" element={ <Route
path="/pensao-alimenticia/nova"
element={
<ProtectedRoute> <ProtectedRoute>
<NovoPensaoAlimenticia /> <NovoPensaoAlimenticia />
</ProtectedRoute> </ProtectedRoute>
} /> }
/>
<Route path="/pensao-alimenticia/:id" element={ <Route
path="/pensao-alimenticia/:id"
element={
<ProtectedRoute> <ProtectedRoute>
<DetalhesCalculo /> <DetalhesCalculo />
</ProtectedRoute> </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,4 +1,5 @@
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
@@ -0,0 +1,120 @@
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>
);
};
+3 -2
View File
@@ -1,5 +1,6 @@
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 />;
+86 -57
View File
@@ -1,12 +1,21 @@
import { useState, useEffect, useCallback } from 'react'; import { Dialog } from "@headlessui/react";
import { useParams, useNavigate, Link } from 'react-router-dom'; import {
import { DocumentIcon, PlusIcon, TrashIcon, EyeIcon, XMarkIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline'; DocumentIcon,
import { DocumentoEditor } from './documentos/DocumentoEditor'; PlusIcon,
import { api } from '../lib/api'; TrashIcon,
import { toast } from 'react-toastify'; EyeIcon,
import { useAuth } from '../contexts/AuthContext'; XMarkIcon,
import { Dialog } from '@headlessui/react'; DocumentArrowDownIcon,
import { DocumentoViewer } from './documentos/DocumentoViewer'; } from "@heroicons/react/24/outline";
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;
@@ -39,7 +48,7 @@ export function DocumentosPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!user) { if (!user) {
navigate('/login'); navigate("/login");
return; return;
} }
@@ -47,12 +56,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);
@@ -68,12 +77,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);
}; };
@@ -82,40 +91,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");
} }
} }
}; };
@@ -125,34 +134,38 @@ 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('/documentos/exportar-pdf', { const response = await api.post(
"/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");
} }
}; };
@@ -162,10 +175,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);
@@ -176,9 +189,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);
}; };
@@ -199,9 +212,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" />
@@ -217,8 +230,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
@@ -227,8 +240,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
@@ -239,18 +252,24 @@ 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 key={doc.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4"> <div
<div className="p-4 min-h-[180px] flex flex-col justify-between"> {/* Ajustado min-height e layout */} key={doc.id}
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">{doc.nome}</h3> <h3 className="font-semibold text-gray-900 dark:text-gray-100 line-clamp-2">
{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
@@ -284,13 +303,20 @@ 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 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
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">{modelo.nome}</h3> <h3 className="font-semibold text-gray-900 dark:text-gray-100">
{modelo.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{modelo.descricao}</p> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{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"
@@ -327,13 +353,17 @@ 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={{ __html: documentoVisualizado?.conteudo }} dangerouslySetInnerHTML={{
__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: {documentoVisualizado && new Date(documentoVisualizado.criadoEm).toLocaleDateString()} Criado em:{" "}
{documentoVisualizado &&
new Date(documentoVisualizado.criadoEm).toLocaleDateString()}
</div> </div>
<button <button
@@ -342,7 +372,6 @@ export function DocumentosPage() {
> >
Editar Editar
</button> </button>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</div> </div>
+17 -7
View File
@@ -1,6 +1,7 @@
import React from 'react'; import React from "react";
import { Card } from './ui/Card'; import { isRouteErrorResponse, useRouteError } from "react-router-dom";
import { isRouteErrorResponse, useRouteError } from 'react-router-dom';
import { Card } from "./ui/Card";
export function ErrorBoundary() { export function ErrorBoundary() {
const error = useRouteError(); const error = useRouteError();
@@ -18,15 +19,24 @@ 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><strong>Status:</strong> {error.status}</p> <p>
<p><strong>Mensagem:</strong> {error.data.message || error.statusText}</p> <strong>Status:</strong> {error.status}
</p>
<p>
<strong>Mensagem:</strong>{" "}
{error.data.message || error.statusText}
</p>
</div> </div>
) : error instanceof Error ? ( ) : error instanceof Error ? (
<div> <div>
<p><strong>Erro:</strong> {error.message}</p> <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">Ver detalhes técnicos</summary> <summary className="cursor-pointer text-sm">
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,5 +1,6 @@
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,5 +1,6 @@
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 = () => {
+12 -11
View File
@@ -1,5 +1,6 @@
import { useAuth } from '../contexts/AuthContext'; import { Navigate, useLocation } from "react-router-dom";
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from "../contexts/AuthContext";
interface ProtectedRouteProps { interface ProtectedRouteProps {
children: React.ReactNode; children: React.ReactNode;
@@ -10,27 +11,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,4 +1,3 @@
import { useLocation } from 'react-router-dom';
import { import {
HomeIcon, HomeIcon,
ScaleIcon, ScaleIcon,
@@ -6,20 +5,21 @@ 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,
})); }));
} }
+37 -21
View File
@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from 'react'; import { DateTimePicker } from "@mui/x-date-pickers";
import { DateTimePicker } from '@mui/x-date-pickers'; import { useState, useCallback, useEffect } from "react";
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) {
@@ -28,18 +29,24 @@ interface AgendamentoFormProps {
}; };
} }
export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClienteId, agendamento }: AgendamentoFormProps) { export function AgendamentoForm({
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(() => {
@@ -47,8 +54,8 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
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]);
@@ -60,16 +67,16 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
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;
} }
@@ -77,7 +84,7 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
setIsSaving(true); setIsSaving(true);
if (agendamento) { if (agendamento) {
// Atualizar agendamento existente // Atualizar agendamento existente
await api.put(`/agendamentos/${agendamento.id}`, { await api.put(`/atendimentos/agendamentos/${agendamento.id}`, {
clienteId, clienteId,
data: data.toISOString(), data: data.toISOString(),
observacoes, observacoes,
@@ -85,7 +92,7 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
}); });
} else { } else {
// Criar novo agendamento // Criar novo agendamento
await api.post('/agendamentos', { await api.post("/atendimentos/agendamentos", {
clienteId, clienteId,
data: data.toISOString(), data: data.toISOString(),
observacoes, observacoes,
@@ -95,14 +102,21 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
onSave(); onSave();
onClose(); onClose();
} catch (error) { } catch (error) {
setError(agendamento ? 'Erro ao atualizar agendamento' : 'Erro ao criar agendamento'); console.error("Erro ao salvar agendamento:", error);
setError(
agendamento
? "Erro ao atualizar agendamento"
: "Erro ao criar agendamento",
);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
return ( return (
<div className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? 'flex' : 'hidden'} z-50`}> <div
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>
@@ -125,7 +139,7 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
{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"
@@ -137,7 +151,9 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
}} }}
> >
<div className="font-medium">{client.nome}</div> <div className="font-medium">{client.nome}</div>
<div className="text-sm text-gray-500">{client.cpfCnpj}</div> <div className="text-sm text-gray-500">
{client.cpfCnpj}
</div>
</div> </div>
))} ))}
</div> </div>
@@ -198,7 +214,7 @@ export function AgendamentoForm({ open, onClose, onSave, clienteId: defaultClien
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,19 +1,23 @@
import { useState } from 'react'; import { format } from "date-fns";
import { useQuery } from 'react-query'; import { ptBR } from "date-fns/locale";
import { useParams, Link } from 'react-router-dom'; import { useState } from "react";
import api from '../../services/api'; import { useQuery } from "react-query";
import { format, parseISO } from 'date-fns'; import { useParams, Link } from "react-router-dom";
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 = (date: string | Date | null | undefined, formatStr: string): string => { const formatDate = (
if (!date) return 'Data não disponível'; date: string | Date | null | undefined,
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";
} }
}; };
@@ -21,31 +25,34 @@ 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(['atendimento', id], async () => { const { data: atendimento, refetch } = useQuery(
["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);
} }
@@ -66,12 +73,20 @@ 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">{atendimento.titulo}</h1> <h1 className="text-2xl font-bold text-white">
<p className="text-blue-100">Protocolo: {atendimento.protocolo}</p> {atendimento.titulo}
</h1>
<p className="text-blue-100">
Protocolo: {atendimento.protocolo}
</p>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${ <span
atendimento.tipo === 'INICIAL' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800' 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} {atendimento.tipo}
</span> </span>
</div> </div>
@@ -82,17 +97,23 @@ 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">{atendimento.cliente.nome}</p> <p className="mt-1 text-sm text-gray-900">
{atendimento.cliente.nome}
</p>
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-gray-500">Data do Atendimento</h3> <h3 className="text-sm font-medium text-gray-500">
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">Processo Vinculado</h3> <h3 className="text-sm font-medium text-gray-500">
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"
@@ -103,7 +124,9 @@ 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">{atendimento.usuario.nome}</p> <p className="mt-1 text-sm text-gray-900">
{atendimento.usuario.nome}
</p>
</div> </div>
</div> </div>
@@ -122,9 +145,22 @@ 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 key={anexo.id} className="flex items-center p-4 bg-gray-50 rounded-lg"> <div
<svg className="w-6 h-6 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> key={anexo.id}
<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" /> className="flex items-center p-4 bg-gray-50 rounded-lg"
>
<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">
@@ -137,7 +173,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"
> >
@@ -150,7 +186,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"
@@ -163,9 +199,12 @@ 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">Cancelar Atendimento</h3> <h3 className="text-lg font-medium mb-4">
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 como cancelado, mas manterá seu protocolo e histórico. Esta ação não pode ser desfeita. O atendimento será marcado
como cancelado, mas manterá seu protocolo e histórico.
</p> </p>
<textarea <textarea
value={justificativa} value={justificativa}
@@ -187,7 +226,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>
@@ -195,10 +234,14 @@ 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">Atendimento Cancelado</h4> <h4 className="text-red-800 font-medium">
<p className="text-red-600 mt-1">{atendimento.justificativaCancelamento}</p> Atendimento Cancelado
</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>
+131 -75
View File
@@ -1,13 +1,26 @@
import { useState, useEffect, useCallback } from 'react'; import { XMarkIcon, PaperClipIcon } from "@heroicons/react/24/outline"; // Alterado para v2
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, FormControl, InputLabel, Select, MenuItem, Alert } from '@mui/material'; import {
import { useEditor, EditorContent } from '@tiptap/react'; Dialog,
import StarterKit from '@tiptap/starter-kit'; DialogTitle,
import Link from '@tiptap/extension-link'; DialogContent,
import { TipoAtendimento } from '@prisma/client'; DialogActions,
import api from '../../services/api'; // Adicionar esta importação Button,
import { XMarkIcon, PaperClipIcon } from '@heroicons/react/24/outline'; // Alterado para v2 TextField,
import { useDropzone } from 'react-dropzone'; FormControl,
import { uploadFile } from '../../services/upload'; InputLabel,
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) {
@@ -40,19 +53,26 @@ interface AnexoTemp {
erro?: string; erro?: string;
} }
export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId, initialData }: AtendimentoFormProps) { export function AtendimentoForm({
const [searchTerm, setSearchTerm] = useState(''); open,
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);
@@ -61,41 +81,42 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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: initialData.atendimentoInicialId || prev.atendimentoInicialId 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();
@@ -109,18 +130,21 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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.get(`/atendimentos/iniciais/${formData.clienteId}`) api
.then(response => setAtendimentosIniciais(response.data)) .get(`/atendimentos/iniciais/${formData.clienteId}`)
.catch(error => console.error('Erro ao carregar atendimentos:', error)); .then((response) => setAtendimentosIniciais(response.data))
.catch((error) =>
console.error("Erro ao carregar atendimentos:", error),
);
} }
}, [formData.clienteId, formData.tipo]); }, [formData.clienteId, formData.tipo]);
@@ -131,28 +155,28 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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) => {
@@ -161,10 +185,10 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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;
} }
@@ -175,15 +199,15 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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,
}); });
@@ -195,15 +219,15 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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;
} }
}); });
@@ -213,38 +237,42 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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 className={`fixed inset-0 bg-gray-500 bg-opacity-75 ${open ? 'flex' : 'hidden'} z-50`}> <div
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 onClick={onClose} className="text-gray-400 hover:text-gray-500"> <button
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>
@@ -268,19 +296,24 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
{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 => ({ ...prev, clienteId: client.id })); setFormData((prev) => ({
...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">{client.cpfCnpj}</div> <div className="text-sm text-gray-500">
{client.cpfCnpj}
</div>
</div> </div>
))} ))}
</div> </div>
@@ -294,12 +327,17 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
</label> </label>
<select <select
value={formData.tipo} value={formData.tipo}
onChange={(e) => setFormData({ ...formData, tipo: e.target.value as TipoAtendimento })} onChange={(e) =>
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>
@@ -313,7 +351,12 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
</label> </label>
<select <select
value={formData.atendimentoInicialId} value={formData.atendimentoInicialId}
onChange={(e) => setFormData({ ...formData, atendimentoInicialId: e.target.value })} onChange={(e) =>
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}
> >
@@ -331,7 +374,9 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
fullWidth fullWidth
label="Título do Atendimento" label="Título do Atendimento"
value={formData.titulo} value={formData.titulo}
onChange={(e) => setFormData({ ...formData, titulo: e.target.value })} onChange={(e) =>
setFormData({ ...formData, titulo: e.target.value })
}
required required
/> />
@@ -345,14 +390,14 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
<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>
@@ -371,14 +416,16 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
<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">Arraste arquivos aqui ou clique para selecionar</p> <p className="pl-1">
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>
@@ -388,17 +435,26 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
{/* 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">Arquivos para upload</h4> <h4 className="text-sm font-medium text-gray-700">
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 key={index} className="py-3 flex justify-between items-center"> <li
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">{anexo.nome}</span> <span className="text-sm text-gray-900">
{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">{anexo.erro}</span> <span className="text-xs text-red-500 mr-2">
{anexo.erro}
</span>
)} )}
<button <button
type="button" type="button"
@@ -434,7 +490,7 @@ export function AtendimentoForm({ open, onClose, onSave, clienteId, processoId,
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,46 +1,59 @@
import { useState } from 'react'; import {
import { useQuery } from 'react-query'; TextField,
import { useNavigate } from 'react-router-dom'; FormControl,
import api from '../../services/api'; InputLabel,
import { AtendimentoForm } from './AtendimentoForm'; Select,
import { AgendamentoCalendario } from './AgendamentoCalendario'; // Adicionar esta importação MenuItem,
import { formatRelative } from 'date-fns'; } from "@mui/material";
import { ptBR } from 'date-fns/locale'; import { DatePicker } from "@mui/x-date-pickers";
import { TextField, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; import { TipoAtendimento } from "@prisma/client";
import { DatePicker } from '@mui/x-date-pickers'; import { formatRelative } from "date-fns";
import { TipoAtendimento } from '@prisma/client'; import { ptBR } from "date-fns/locale";
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, dataInicio: null as Date | null,
dataFim: null, dataFim: null as Date | null,
tipo: '', tipo: "",
clienteId: '', clienteId: "",
processoId: '', processoId: "",
}); });
const { data: atendimentos, refetch } = useQuery(['atendimentos', filtros], async () => { const { data: atendimentos, refetch } = useQuery(
["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) params.append('dataInicio', filtros.dataInicio.toISOString()); if (filtros.dataInicio)
if (filtros.dataFim) params.append('dataFim', filtros.dataFim.toISOString()); params.append("dataInicio", filtros.dataInicio.toISOString());
if (filtros.tipo) params.append('tipo', filtros.tipo); if (filtros.dataFim)
if (filtros.clienteId) params.append('clienteId', filtros.clienteId); params.append("dataFim", filtros.dataFim.toISOString());
if (filtros.processoId) params.append('processoId', filtros.processoId); if (filtros.tipo) params.append("tipo", filtros.tipo);
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 (data: any) => { const handleSave = async () => {
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);
} }
}; };
@@ -48,7 +61,9 @@ 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">Atendimentos</h1> <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
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"
@@ -68,7 +83,9 @@ export const AtendimentosPage = () => {
<TextField <TextField
label="Protocolo" label="Protocolo"
value={filtros.protocolo} value={filtros.protocolo}
onChange={(e) => setFiltros({ ...filtros, protocolo: e.target.value })} onChange={(e) =>
setFiltros({ ...filtros, protocolo: e.target.value })
}
size="small" size="small"
/> />
@@ -76,14 +93,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">
@@ -91,12 +108,14 @@ export const AtendimentosPage = () => {
<Select <Select
value={filtros.tipo} value={filtros.tipo}
label="Tipo" label="Tipo"
onChange={(e) => setFiltros({ ...filtros, tipo: e.target.value })} onChange={(e) =>
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>
@@ -109,15 +128,19 @@ 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' ? 'border-l-4 border-red-500' : '' atendimento.status === "CANCELADO"
? "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">{atendimento.titulo}</h2> <h2 className="text-lg font-medium text-gray-900 dark:text-white">
{atendimento.status === 'CANCELADO' && ( {atendimento.titulo}
</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>
@@ -127,27 +150,48 @@ export const AtendimentosPage = () => {
Protocolo: {atendimento.protocolo} Protocolo: {atendimento.protocolo}
</p> </p>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${ <span
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' 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} {atendimento.tipo}
</span> </span>
</div> </div>
<div className="mt-4 prose max-w-none" dangerouslySetInnerHTML={{ __html: atendimento.conteudo }} /> <div
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(new Date(atendimento.dataAtendimento), new Date(), { locale: ptBR })} {formatRelative(
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 className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<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" /> className="w-4 h-4 mr-1"
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>
+70 -45
View File
@@ -1,9 +1,10 @@
import { useEffect, useState } from 'react'; import { format } from "date-fns";
import { Link, useNavigate } from 'react-router-dom'; import { ptBR } from "date-fns/locale";
import { format } from 'date-fns'; import { useEffect, useState } from "react";
import ptBR from 'date-fns/locale/pt-BR'; import { Link, useNavigate } from "react-router-dom";
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;
@@ -32,10 +33,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(() => {
@@ -44,7 +45,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]);
@@ -54,46 +55,49 @@ 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) queryParams.append('processoId', filtro.processoId); if (filtro.processoId)
if (filtro.clienteId) queryParams.append('clienteId', filtro.clienteId); queryParams.append("processoId", filtro.processoId);
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 = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleFiltroChange = (
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) => {
@@ -103,18 +107,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";
} }
}; };
@@ -122,19 +126,21 @@ 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 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor()}`}> <span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor()}`}
>
{status} {status}
</span> </span>
); );
@@ -155,9 +161,15 @@ export function CalculosList() {
{/* 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 onSubmit={handleSubmitFiltro} className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <form
onSubmit={handleSubmitFiltro}
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"
>
<div> <div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700"> <label
htmlFor="titulo"
className="block text-sm font-medium text-gray-700"
>
Título Título
</label> </label>
<input <input
@@ -171,7 +183,10 @@ export function CalculosList() {
</div> </div>
<div> <div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700"> <label
htmlFor="status"
className="block text-sm font-medium text-gray-700"
>
Status Status
</label> </label>
<select <select
@@ -202,13 +217,18 @@ 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 className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600" role="status"> <div
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">Nenhum cálculo encontrado</h3> <h3 className="text-lg font-medium text-gray-900">
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>
@@ -218,7 +238,10 @@ 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 to={`/calculos/${calculo.id}`} className="block hover:bg-gray-50"> <Link
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">
@@ -244,14 +267,16 @@ 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 {calculo.usuario.nome} Criado em: {formatarData(calculo.criadoEm)} por{" "}
{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} - {calculo.processo.titulo} Processo: {calculo.processo.numero} -{" "}
{calculo.processo.titulo}
</p> </p>
</div> </div>
)} )}
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import { useParams } from 'react-router-dom'; import { useParams } from "react-router-dom";
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;
@@ -36,8 +37,8 @@ export function DetalhePensaoAlimenticia() {
const response = await api.get(`/pensao-alimenticia/${id}`); const response = await api.get(`/pensao-alimenticia/${id}`);
setCalculo(response.data); setCalculo(response.data);
} catch (error) { } catch (error) {
console.error('Erro ao carregar cálculo:', error); console.error("Erro ao carregar cálculo:", error);
toast.error('Falha ao carregar dados do cálculo'); toast.error("Falha ao carregar dados do cálculo");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -62,24 +63,48 @@ 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><span className="font-medium">Data Inicial:</span> {new Date(calculo.dataInicial).toLocaleDateString()}</p> <p>
<span className="font-medium">Data Inicial:</span>{" "}
{new Date(calculo.dataInicial).toLocaleDateString()}
</p>
{calculo.dataFinal && ( {calculo.dataFinal && (
<p><span className="font-medium">Data Final:</span> {new Date(calculo.dataFinal).toLocaleDateString()}</p> <p>
<span className="font-medium">Data Final:</span>{" "}
{new Date(calculo.dataFinal).toLocaleDateString()}
</p>
)} )}
<p><span className="font-medium">Percentual:</span> {calculo.percentualSalarioMinimo}%</p> <p>
<span className="font-medium">Percentual:</span>{" "}
{calculo.percentualSalarioMinimo}%
</p>
{calculo.multaPactuada && ( {calculo.multaPactuada && (
<p><span className="font-medium">Multa:</span> {calculo.multaPactuada}%</p> <p>
<span className="font-medium">Multa:</span>{" "}
{calculo.multaPactuada}%
</p>
)} )}
<p><span className="font-medium">Índice:</span> {calculo.tipoAtualizacao}</p> <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><span className="font-medium">Valor Base:</span> R$ {calculo.valorCalculado.toFixed(2)}</p> <p>
<p><span className="font-medium">Valor Multa:</span> R$ {calculo.valorMulta.toFixed(2)}</p> <span className="font-medium">Valor Base:</span> R${" "}
<p><span className="font-medium">Valor Total:</span> R$ {calculo.valorTotal.toFixed(2)}</p> {calculo.valorCalculado.toFixed(2)}
</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>
File diff suppressed because it is too large. Load diff
+101 -51
View File
@@ -1,8 +1,9 @@
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;
@@ -21,12 +22,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[]>([]);
@@ -37,30 +38,34 @@ 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 = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleChange = (
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
@@ -72,19 +77,22 @@ 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(0, (cursorPosition || 0) + lengthDifference); const newPosition = Math.max(
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);
@@ -99,7 +107,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;
} }
@@ -107,7 +115,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;
} }
@@ -115,45 +123,47 @@ 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 (error.message?.includes('autenticação') || if (
error.message?.includes('não autenticado') || error.message?.includes("autenticação") ||
error.response?.status === 401) { error.message?.includes("não autenticado") ||
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);
@@ -164,14 +174,19 @@ 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">Preencha os dados básicos para criar um novo cálculo</p> <p className="text-sm text-blue-100 mt-1">
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 htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-1"> <label
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
@@ -188,7 +203,10 @@ export function NovoCalculo() {
{/* Data Base */} {/* Data Base */}
<div> <div>
<label htmlFor="dataBase" className="block text-sm font-medium text-gray-700 mb-1"> <label
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
@@ -200,12 +218,17 @@ 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">Data de referência para iniciar o cálculo</p> <p className="mt-1 text-xs text-gray-500">
Data de referência para iniciar o cálculo
</p>
</div> </div>
{/* Valor Base */} {/* Valor Base */}
<div> <div>
<label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1"> <label
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">
@@ -224,12 +247,17 @@ export function NovoCalculo() {
required required
/> />
</div> </div>
<p className="mt-1 text-xs text-gray-500">Valor inicial para o cálculo</p> <p className="mt-1 text-xs text-gray-500">
Valor inicial para o cálculo
</p>
</div> </div>
{/* Processo */} {/* Processo */}
<div> <div>
<label htmlFor="processoId" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="processoId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Processo Processo
</label> </label>
<select <select
@@ -240,7 +268,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>
@@ -250,7 +278,10 @@ export function NovoCalculo() {
{/* Cliente */} {/* Cliente */}
<div> <div>
<label htmlFor="clienteId" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="clienteId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Cliente Cliente
</label> </label>
<select <select
@@ -261,7 +292,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>
@@ -271,7 +302,10 @@ export function NovoCalculo() {
{/* Descrição */} {/* Descrição */}
<div className="col-span-2"> <div className="col-span-2">
<label htmlFor="descricao" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="descricao"
className="block text-sm font-medium text-gray-700 mb-1"
>
Descrição Descrição
</label> </label>
<textarea <textarea
@@ -289,7 +323,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"
> >
@@ -301,12 +335,28 @@ 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 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"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
<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> 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> </svg>
)} )}
{loading ? 'Criando...' : 'Criar Cálculo'} {loading ? "Criando..." : "Criar Cálculo"}
</button> </button>
</div> </div>
</form> </form>
+135 -58
View File
@@ -1,7 +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 { formatarPercentual, parsePercentual } from '../../utils/formatters';
import { formatarPercentual, parsePercentual } from "../../utils/formatters";
interface ProcessoOption { interface ProcessoOption {
id: string; id: string;
@@ -18,16 +19,16 @@ export function NovoPensaoAlimenticia() {
const navigate = useNavigate(); const navigate = useNavigate();
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[]>([]);
@@ -38,40 +39,44 @@ 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 = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { const handleChange = (
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) => {
@@ -80,8 +85,12 @@ export function NovoPensaoAlimenticia() {
try { try {
// Validar dados // Validar dados
if (!formData.titulo || !formData.dataInicial || !formData.percentualSalarioMinimo) { if (
toast.error('Preencha todos os campos obrigatórios'); !formData.titulo ||
!formData.dataInicial ||
!formData.percentualSalarioMinimo
) {
toast.error("Preencha todos os campos obrigatórios");
setLoading(false); setLoading(false);
return; return;
} }
@@ -91,7 +100,9 @@ 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('Percentual do salário mínimo deve ser maior que 0 e menor ou igual a 100%'); toast.error(
"Percentual do salário mínimo deve ser maior que 0 e menor ou igual a 100%",
);
setLoading(false); setLoading(false);
return; return;
} }
@@ -100,31 +111,35 @@ export function NovoPensaoAlimenticia() {
const dadosParaEnviar = { const dadosParaEnviar = {
...formData, ...formData,
percentualSalarioMinimo: percentual, percentualSalarioMinimo: percentual,
multaPactuada: formData.multaPactuada ? parsePercentual(formData.multaPactuada) : undefined, multaPactuada: formData.multaPactuada
? parsePercentual(formData.multaPactuada)
: undefined,
}; };
// 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",
}, },
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 de pensão alimentícia'); throw new Error(
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);
@@ -134,15 +149,22 @@ 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">Novo Cálculo de Pensão Alimentícia</h1> <h1 className="text-xl font-bold">
<p className="text-sm text-blue-100 mt-1">Preencha os dados para criar um novo cálculo de pensão alimentícia</p> Novo Cálculo de Pensão Alimentícia
</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 htmlFor="titulo" className="block text-sm font-medium text-gray-700 mb-1"> <label
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
@@ -159,7 +181,10 @@ export function NovoPensaoAlimenticia() {
{/* Índice de Atualização */} {/* Índice de Atualização */}
<div> <div>
<label htmlFor="tipoAtualizacao" className="block text-sm font-medium text-gray-700 mb-1"> <label
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
@@ -169,17 +194,29 @@ 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">INPC (Índice Nacional de Preços ao Consumidor)</option> <option value="INPC">
<option value="IPCA">IPCA (Índice Nacional de Preços ao Consumidor Amplo)</option> INPC (Índice Nacional de Preços ao Consumidor)
<option value="IGPM">IGP-M (Índice Geral de Preços do Mercado)</option> </option>
<option value="SELIC">SELIC (Sistema Especial de Liquidação e Custódia)</option> <option value="IPCA">
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 htmlFor="percentualSalarioMinimo" className="block text-sm font-medium text-gray-700 mb-1"> <label
Percentual do Salário Mínimo <span className="text-red-500">*</span> htmlFor="percentualSalarioMinimo"
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
@@ -201,7 +238,10 @@ export function NovoPensaoAlimenticia() {
{/* Data Inicial */} {/* Data Inicial */}
<div> <div>
<label htmlFor="dataInicial" className="block text-sm font-medium text-gray-700 mb-1"> <label
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
@@ -213,12 +253,17 @@ 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">Data de início da pensão</p> <p className="mt-1 text-xs text-gray-500">
Data de início da pensão
</p>
</div> </div>
{/* Data Final */} {/* Data Final */}
<div> <div>
<label htmlFor="dataFinal" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="dataFinal"
className="block text-sm font-medium text-gray-700 mb-1"
>
Data Final Data Final
</label> </label>
<input <input
@@ -229,12 +274,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"
/> />
<p className="mt-1 text-xs text-gray-500">Deixe em branco se a pensão não tem data final</p> <p className="mt-1 text-xs text-gray-500">
Deixe em branco se a pensão não tem data final
</p>
</div> </div>
{/* Multa Pactuada */} {/* Multa Pactuada */}
<div> <div>
<label htmlFor="multaPactuada" className="block text-sm font-medium text-gray-700 mb-1"> <label
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">
@@ -251,12 +301,17 @@ 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">Ex: 10% = 10,00%. Deixe em branco se não houver.</p> <p className="mt-1 text-xs text-gray-500">
Ex: 10% = 10,00%. Deixe em branco se não houver.
</p>
</div> </div>
{/* Processo */} {/* Processo */}
<div> <div>
<label htmlFor="processoId" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="processoId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Processo Processo
</label> </label>
<select <select
@@ -267,7 +322,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>
@@ -277,7 +332,10 @@ export function NovoPensaoAlimenticia() {
{/* Cliente */} {/* Cliente */}
<div> <div>
<label htmlFor="clienteId" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="clienteId"
className="block text-sm font-medium text-gray-700 mb-1"
>
Cliente Cliente
</label> </label>
<select <select
@@ -288,7 +346,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>
@@ -298,7 +356,10 @@ export function NovoPensaoAlimenticia() {
{/* Observações */} {/* Observações */}
<div className="col-span-2"> <div className="col-span-2">
<label htmlFor="observacoes" className="block text-sm font-medium text-gray-700 mb-1"> <label
htmlFor="observacoes"
className="block text-sm font-medium text-gray-700 mb-1"
>
Observações Observações
</label> </label>
<textarea <textarea
@@ -316,7 +377,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"
> >
@@ -328,12 +389,28 @@ 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 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"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
<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> 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> </svg>
)} )}
{loading ? 'Criando...' : 'Criar Cálculo de Pensão'} {loading ? "Criando..." : "Criar Cálculo de Pensão"}
</button> </button>
</div> </div>
</form> </form>
+6 -4
View File
@@ -1,6 +1,3 @@
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,
@@ -11,7 +8,10 @@ import {
UserGroupIcon, UserGroupIcon,
DocumentTextIcon, DocumentTextIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ClienteModal } from "./ClienteModal"; import React, { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useClientes } from "../../hooks/useClientes";
import { import {
formatCPF, formatCPF,
formatTelefone, formatTelefone,
@@ -22,6 +22,8 @@ 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(" ");
} }
+65 -21
View File
@@ -1,3 +1,6 @@
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,
@@ -5,12 +8,10 @@ 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 { Cliente } from "../../types/entities";
import { useClientes } from "../../hooks/useClientes"; import { useClientes } from "../../hooks/useClientes";
import { Cliente } from "../../types/entities";
import { ORGAOS_EMISSORES, PREVIDENCIA_OPTIONS } from "../../utils/constants"; import { ORGAOS_EMISSORES, PREVIDENCIA_OPTIONS } from "../../utils/constants";
const estadosCivis = [ const estadosCivis = [
@@ -200,8 +201,9 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
// Monitorar o valor do CEP para autopreenchimento // Monitorar o valor do CEP para autopreenchimento
const cepValue = watch("cep"); const cepValue = watch("cep");
// Função para buscar dados do CEP - versão com gerenciamento de estados do componente // Função para buscar dados do CEP - versão corrigida
const fetchAddressByCep = useCallback(async (cep: string) => { const fetchAddressByCep = useCallback(
async (cep: string) => {
if (!cep || cep.length !== 8) return; if (!cep || cep.length !== 8) return;
try { try {
@@ -213,7 +215,6 @@ 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 // Usar estado local para garantir re-renderização imediata
// Forçar preenchimento dos campos e atualização da interface
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 || "";
@@ -225,8 +226,7 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
setValue("cidade", cidade, { shouldValidate: true }); setValue("cidade", cidade, { shouldValidate: true });
setValue("estado", estado, { shouldValidate: true }); setValue("estado", estado, { shouldValidate: true });
// Garantir re-renderização do componente // Atualizar o estado para re-renderização do componente
// Esta é a parte crucial: forçar o componente a re-renderizar com novo estado
setEnderecoState({ setEnderecoState({
endereco, endereco,
bairro, bairro,
@@ -235,13 +235,24 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
preenchido: true, preenchido: true,
}); });
// Focar no próximo campo // Focar no próximo campo apenas se o usuário não estiver digitando ativamente
// Esta alteração evita interromper o fluxo natural de preenchimento
const activeElement = document.activeElement;
const isUserTyping =
activeElement &&
(activeElement.tagName === "INPUT" ||
activeElement.tagName === "SELECT") &&
activeElement !== document.querySelector('input[name="cep"]');
// Só focar no campo número se o usuário não estiver digitando em outro campo
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 {
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.");
@@ -252,7 +263,9 @@ 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);
@@ -576,7 +589,10 @@ 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(/\D/g, ""); const cleanCep = e.target.value.replace(
/\D/g,
"",
);
if (cleanCep && cleanCep.length === 8) { if (cleanCep && cleanCep.length === 8) {
fetchAddressByCep(cleanCep); fetchAddressByCep(cleanCep);
} }
@@ -614,10 +630,17 @@ 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={enderecoState.preenchido ? enderecoState.endereco : undefined} value={
enderecoState.preenchido
? enderecoState.endereco
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("endereco", e.target.value); setValue("endereco", e.target.value);
setEnderecoState(prev => ({...prev, endereco: e.target.value})); setEnderecoState((prev) => ({
...prev,
endereco: e.target.value,
}));
}} }}
/> />
<FormInput <FormInput
@@ -637,10 +660,17 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
required: "Bairro é obrigatório", required: "Bairro é obrigatório",
})} })}
error={errors.bairro?.message} error={errors.bairro?.message}
value={enderecoState.preenchido ? enderecoState.bairro : undefined} value={
enderecoState.preenchido
? enderecoState.bairro
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("bairro", e.target.value); setValue("bairro", e.target.value);
setEnderecoState(prev => ({...prev, bairro: e.target.value})); setEnderecoState((prev) => ({
...prev,
bairro: e.target.value,
}));
}} }}
/> />
<FormInput <FormInput
@@ -649,10 +679,17 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
required: "Cidade é obrigatória", required: "Cidade é obrigatória",
})} })}
error={errors.cidade?.message} error={errors.cidade?.message}
value={enderecoState.preenchido ? enderecoState.cidade : undefined} value={
enderecoState.preenchido
? enderecoState.cidade
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("cidade", e.target.value); setValue("cidade", e.target.value);
setEnderecoState(prev => ({...prev, cidade: e.target.value})); setEnderecoState((prev) => ({
...prev,
cidade: e.target.value,
}));
}} }}
/> />
<FormSelect <FormSelect
@@ -662,10 +699,17 @@ export const ClienteModal: React.FC<ClienteModalProps> = ({
})} })}
options={estadosOptions} options={estadosOptions}
error={errors.estado?.message} error={errors.estado?.message}
value={enderecoState.preenchido ? enderecoState.estado : undefined} value={
enderecoState.preenchido
? enderecoState.estado
: undefined
}
onChange={(e) => { onChange={(e) => {
setValue("estado", e.target.value); setValue("estado", e.target.value);
setEnderecoState(prev => ({...prev, estado: e.target.value})); setEnderecoState((prev) => ({
...prev,
estado: e.target.value,
}));
}} }}
/> />
</div> </div>
+23 -18
View File
@@ -1,16 +1,20 @@
import React, { useState, useEffect } from 'react'; import { PencilIcon } from "@heroicons/react/24/outline";
import { useQuery } from 'react-query'; import React, { useState, useEffect } from "react";
import { useNavigate } from 'react-router-dom'; import { useQuery } from "react-query";
import { PencilIcon } from '@heroicons/react/24/outline'; import { useNavigate } from "react-router-dom";
import api from '../../services/api';
import { formatCPF, formatTelefone } from '../../utils/formatters'; import api from "../../services/api";
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> = ({ searchTerm, onEdit }) => { export const ClientesList: React.FC<ClientesListProps> = ({
searchTerm,
onEdit,
}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [debouncedTerm, setDebouncedTerm] = useState(searchTerm); const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
@@ -24,37 +28,38 @@ export const ClientesList: React.FC<ClientesListProps> = ({ searchTerm, onEdit }
}, [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(term => searchTerms.every(
(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) {
+5 -3
View File
@@ -1,9 +1,11 @@
import React, { useState } from "react";
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { ClientesList } from "./ClientesList.tsx"; import React, { useState } from "react";
import { ClienteModal } from "./ClienteModal.tsx";
import { Cliente } from "../../types/entities"; import { Cliente } from "../../types/entities";
import { ClienteModal } from "./ClienteModal.tsx";
import { ClientesList } from "./ClientesList.tsx";
export const ClientesPage: React.FC = () => { export const ClientesPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
+20 -13
View File
@@ -1,20 +1,23 @@
import React from 'react'; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { useQuery } from 'react-query'; import { format, differenceInDays } from "date-fns";
import { format, differenceInDays } from 'date-fns'; import { ptBR } from "date-fns/locale";
import { ptBR } from 'date-fns/locale'; import React from "react";
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { useQuery } from "react-query";
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) return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; if (dias <= 3)
if (dias <= 7) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; if (dias <= 7)
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
}; };
return ( return (
@@ -38,8 +41,12 @@ export const AlertasPrazos: React.FC = () => {
Processo: {prazo.processo.numero} Processo: {prazo.processo.numero}
</p> </p>
</div> </div>
<span className={`px-2 py-1 rounded-full text-xs ${getPrioridadeStyle(dias)}`}> <span
{format(new Date(prazo.dataLimite), 'dd/MM/yyyy', { locale: ptBR })} className={`px-2 py-1 rounded-full text-xs ${getPrioridadeStyle(dias)}`}
>
{format(new Date(prazo.dataLimite), "dd/MM/yyyy", {
locale: ptBR,
})}
</span> </span>
</div> </div>
); );
+17 -12
View File
@@ -1,22 +1,25 @@
import React from 'react'; import { format } from "date-fns";
import { useQuery } from 'react-query'; import { ptBR } from "date-fns/locale";
import { format } from 'date-fns'; import React from "react";
import { ptBR } from 'date-fns/locale'; import { useQuery } from "react-query";
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", { locale: ptBR }); return format(new Date(data), "dd 'de' MMMM 'às' HH:mm", {
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";
} }
}; };
@@ -28,7 +31,9 @@ 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">{atividade.tipo === 'PROCESSO' ? '⚖️' : '📋'}</span> <span className="text-2xl">
{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}
+24 -25
View File
@@ -1,8 +1,5 @@
import React from 'react'; import { ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/outline";
import { import React from "react";
ArrowUpIcon,
ArrowDownIcon,
} from '@heroicons/react/24/outline';
interface CardProps { interface CardProps {
title: string; title: string;
@@ -10,9 +7,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> = ({
@@ -21,27 +18,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>
@@ -49,7 +46,9 @@ 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">{item.value}</span> <span className="text-sm font-medium text-gray-900">
{item.value}
</span>
</div> </div>
))} ))}
</div> </div>
@@ -72,7 +71,9 @@ export const Card: React.FC<CardProps> = ({
{value} {value}
</p> </p>
{trend !== undefined && ( {trend !== undefined && (
<div className={`ml-2 flex items-center text-sm ${trend >= 0 ? 'text-green-600' : 'text-red-600'}`}> <div
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" />
) : ( ) : (
@@ -80,9 +81,7 @@ 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"> <span className="ml-1 text-gray-500">{trendLabel}</span>
{trendLabel}
</span>
)} )}
</div> </div>
)} )}
+20 -14
View File
@@ -1,14 +1,16 @@
import React from 'react'; import React from "react";
import { useAuth } from '../../contexts/AuthContext';
import { useDashboardStats } from '../../hooks/useDashboardStats'; import { useAuth } from "../../contexts/AuthContext";
import { ProcessosStats } from './stats/ProcessosStats'; import { useDashboardStats } from "../../hooks/useDashboardStats";
import { TarefasStats } from './stats/TarefasStats';
import { AtendimentosStats } from './stats/AtendimentosStats'; import { AlertasPrazos } from "./AlertasPrazos";
import { FinanceiroStats } from './stats/FinanceiroStats'; import { AtividadesRecentes } from "./AtividadesRecentes";
import { AlertasPrazos } from './AlertasPrazos'; import { AtendimentosStats } from "./stats/AtendimentosStats";
import { AtividadesRecentes } from './AtividadesRecentes'; import { FinanceiroStats } from "./stats/FinanceiroStats";
import { TaxaSucessoCard } from './TaxaSucessoCard'; import { KPIsFinanceiros } from "./stats/KPIsFinanceiros";
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 { user } = useAuth();
@@ -18,10 +20,12 @@ export const DashboardLayout: React.FC = () => {
atendimentosStats, atendimentosStats,
financeiroStats, financeiroStats,
proximosPrazos, proximosPrazos,
atividades atividades,
} = useDashboardStats(); } = useDashboardStats();
const isAdminOrAdvogado = ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user?.perfil || ''); const isAdminOrAdvogado = ["ADVOGADO_ADMIN", "ADVOGADO"].includes(
user?.perfil || "",
);
return ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
@@ -29,7 +33,9 @@ export const DashboardLayout: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{processosStats.data && <ProcessosStats data={processosStats.data} />} {processosStats.data && <ProcessosStats data={processosStats.data} />}
{tarefasStats.data && <TarefasStats data={tarefasStats.data} />} {tarefasStats.data && <TarefasStats data={tarefasStats.data} />}
{atendimentosStats.data && <AtendimentosStats data={atendimentosStats.data} />} {atendimentosStats.data && (
<AtendimentosStats data={atendimentosStats.data} />
)}
{isAdminOrAdvogado && financeiroStats.data && ( {isAdminOrAdvogado && financeiroStats.data && (
<FinanceiroStats data={financeiroStats.data} /> <FinanceiroStats data={financeiroStats.data} />
)} )}
+22 -9
View File
@@ -1,10 +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";
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;
}); });
@@ -19,16 +20,28 @@ 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 scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Advogado Advogado
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Tarefas Concluídas Tarefas Concluídas
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Processos Concluídos Processos Concluídos
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Produtividade Produtividade
</th> </th>
</tr> </tr>
+23 -13
View File
@@ -1,36 +1,46 @@
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((acc: number, curr: any) => acc + (curr._count || 0), 0); const total = estatisticas.reduce(
(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((estatisticas.find((s: any) => s.resultado === 'PROCEDENTE')._count / total) * 100) ? Math.round(
: 0}% (estatisticas.find((s: any) => s.resultado === "PROCEDENTE")
</div> ._count /
<div className="text-sm text-gray-500"> total) *
Total julgados: {total} 100,
)
: 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">{Math.round((item._count / total) * 100)}%</span> <span className="text-sm font-medium">
{Math.round((item._count / total) * 100)}%
</span>
</div> </div>
))} ))}
</div> </div>
+16 -12
View File
@@ -1,20 +1,24 @@
import React from 'react'; import { format } from "date-fns";
import { useQuery } from 'react-query'; import { ptBR } from "date-fns/locale";
import { format } from 'date-fns'; import React from "react";
import { ptBR } from 'date-fns/locale'; import { useQuery } from "react-query";
import api from '../../../services/api';
import api from "../../../services/api";
export const MapaCalor: React.FC = () => { export const MapaCalor: React.FC = () => {
const { data: atividadesDiarias } = useQuery('atividadesDiarias', async () => { const { data: atividadesDiarias } = useQuery(
const response = await api.get('/dashboard/atividades/diarias'); "atividadesDiarias",
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,19 +1,28 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { useQuery } from "react-query";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import {
import api from '../../../services/api'; BarChart,
import { useAuth } from '../../../contexts/AuthContext'; Bar,
import { formatCurrency } from '../../../utils/formatters'; XAxis,
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,12 +1,15 @@
import React from 'react'; import React from "react";
import { useAuth } from '../../../contexts/AuthContext';
import { ProcessosEvolucao } from './ProcessosEvolucao'; import { useAuth } from "../../../contexts/AuthContext";
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 = user?.perfil === 'ADVOGADO_ADMIN' || user?.perfil === 'ADVOGADO'; const isAdminOrAdvogado =
user?.perfil === "ADVOGADO_ADMIN" || user?.perfil === "ADVOGADO";
return ( return (
<> <>
@@ -1,11 +1,20 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { useQuery } from "react-query";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import {
import api from '../../../services/api'; LineChart,
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;
}); });
@@ -21,8 +30,18 @@ export const ProcessosEvolucao: React.FC = () => {
<XAxis dataKey="mes" /> <XAxis dataKey="mes" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Line type="monotone" dataKey="novos" stroke="#3B82F6" name="Novos" /> <Line
<Line type="monotone" dataKey="concluidos" stroke="#10B981" name="Concluídos" /> type="monotone"
dataKey="novos"
stroke="#3B82F6"
name="Novos"
/>
<Line
type="monotone"
dataKey="concluidos"
stroke="#10B981"
name="Concluídos"
/>
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -1,15 +1,23 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { useQuery } from "react-query";
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; import {
import api from '../../../services/api'; PieChart,
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">
@@ -29,7 +37,10 @@ export const TaxaSucessoChart: React.FC = () => {
label label
> >
{stats?.map((entry: any, index: number) => ( {stats?.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} /> <Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))} ))}
</Pie> </Pie>
<Tooltip /> <Tooltip />
@@ -1,11 +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 { 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,12 +1,13 @@
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,19 +1,20 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { useQuery } from "react-query";
import { useAuth } from '../../../contexts/AuthContext';
import { Card } from '../../dashboard/Card'; import { useAuth } from "../../../contexts/AuthContext";
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,11 +1,12 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { useQuery } from "react-query";
import api from '../../../services/api';
import { Card } from '../Card'; import api from "../../../services/api";
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;
}); });
@@ -17,16 +18,8 @@ export const ProcessosStats: React.FC = () => {
trend={stats?.crescimento || 0} trend={stats?.crescimento || 0}
icon="DocumentText" icon="DocumentText"
/> />
<Card <Card title="Por Tribunal" items={stats?.porTribunal || []} type="list" />
title="Por Tribunal" <Card title="Por Status" items={stats?.porStatus || []} type="list" />
items={stats?.porTribunal || []}
type="list"
/>
<Card
title="Por Status"
items={stats?.porStatus || []}
type="list"
/>
</div> </div>
); );
}; };
@@ -1,22 +1,23 @@
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 },
}; };
} }
}); });
@@ -27,7 +28,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,10 +1,11 @@
import { useState, useEffect } from 'react'; import { Dialog } from "@headlessui/react";
import { api } from '../../lib/api'; import { DocumentArrowUpIcon } from "@heroicons/react/24/outline";
import { toast } from 'react-toastify'; import { Editor } from "@tinymce/tinymce-react";
import { Dialog } from '@headlessui/react'; import { useState, useEffect } from "react";
import { DocumentArrowUpIcon } from '@heroicons/react/24/outline'; import { toast } from "react-toastify";
import { Editor } from '@tinymce/tinymce-react';
import { DOCUMENT_VARIABLES } from '../../utils/documentVariables'; import { api } from "../../lib/api";
import { DOCUMENT_VARIABLES } from "../../utils/documentVariables";
// Definir interface para o estado // Definir interface para o estado
interface ConfigState { interface ConfigState {
@@ -24,18 +25,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);
@@ -47,27 +48,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");
} }
}; };
@@ -76,53 +77,57 @@ export function ConfiguracaoDocumento() {
if (!file) return; if (!file) return;
// Atualizado para aceitar PDF // Atualizado para aceitar PDF
if (!['image/png', 'image/svg+xml', 'application/pdf'].includes(file.type)) { if (
toast.error('Por favor, use apenas arquivos PDF, PNG ou SVG'); !["image/png", "image/svg+xml", "application/pdf"].includes(file.type)
) {
toast.error("Por favor, use apenas arquivos PDF, PNG ou SVG");
return; return;
} }
if (file.size > 5 * 1024 * 1024) { // Aumentado para 5MB para acomodar PDFs if (file.size > 5 * 1024 * 1024) {
toast.error('O arquivo deve ter no máximo 5MB'); // Aumentado para 5MB para acomodar PDFs
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(([category, vars]) => const items = Object.entries(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 as string) onAction: () => editor.insertContent(value as string),
})) })),
); );
callback(items); callback(items);
} },
}); });
}; };
@@ -131,12 +136,14 @@ export function ConfiguracaoDocumento() {
height: 400, height: 400,
menubar: true, menubar: true,
plugins: [ plugins: [
'code', 'preview' // Removido 'template' "code",
"preview", // Removido 'template'
], ],
toolbar: 'variables | undo redo | formatselect | ' + toolbar:
'bold italic | alignleft aligncenter alignright | ' + "variables | undo redo | formatselect | " +
'code preview', "bold italic | alignleft aligncenter alignright | " +
setup: handleEditorSetup "code preview",
setup: handleEditorSetup,
}; };
return ( return (
@@ -179,7 +186,12 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginTop} value={config.timbradoMarginTop}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginTop: Number(e.target.value) }))} onChange={(e) =>
setConfig((prev) => ({
...prev,
timbradoMarginTop: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -188,7 +200,12 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginBottom} value={config.timbradoMarginBottom}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginBottom: Number(e.target.value) }))} onChange={(e) =>
setConfig((prev) => ({
...prev,
timbradoMarginBottom: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -197,7 +214,12 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginLeft} value={config.timbradoMarginLeft}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginLeft: Number(e.target.value) }))} onChange={(e) =>
setConfig((prev) => ({
...prev,
timbradoMarginLeft: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -206,7 +228,12 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.timbradoMarginRight} value={config.timbradoMarginRight}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginRight: Number(e.target.value) }))} onChange={(e) =>
setConfig((prev) => ({
...prev,
timbradoMarginRight: Number(e.target.value),
}))
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
/> />
</div> </div>
@@ -218,7 +245,9 @@ 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) => setConfig(prev => ({ ...prev, templatePadrao: content }))} onEditorChange={(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">
@@ -226,7 +255,9 @@ 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">{category}</h5> <h5 className="font-medium text-sm text-gray-700">
{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">
@@ -247,13 +278,15 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.top} value={config.margensTemplate.top}
onChange={e => setConfig(prev => ({ onChange={(e) =>
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>
@@ -262,13 +295,15 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.bottom} value={config.margensTemplate.bottom}
onChange={e => setConfig(prev => ({ onChange={(e) =>
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>
@@ -277,13 +312,15 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.left} value={config.margensTemplate.left}
onChange={e => setConfig(prev => ({ onChange={(e) =>
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>
@@ -292,13 +329,15 @@ export function ConfiguracaoDocumento() {
<input <input
type="number" type="number"
value={config.margensTemplate.right} value={config.margensTemplate.right}
onChange={e => setConfig(prev => ({ onChange={(e) =>
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>
@@ -309,11 +348,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>
+107 -75
View File
@@ -1,11 +1,18 @@
import { useState, useEffect } from 'react'; import { XMarkIcon } from "@heroicons/react/24/outline";
import { Editor } from '@tinymce/tinymce-react'; import { Save as SaveIcon } from "@mui/icons-material";
import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import {
import { Save as SaveIcon } from '@mui/icons-material'; Button,
import { XMarkIcon } from '@heroicons/react/24/outline'; Dialog,
import { toast } from 'react-toastify'; // Adicionar esta importação DialogTitle,
import { api } from '../../lib/api'; // Adicionar esta importação DialogContent,
import { DocumentoService } from '../../services/DocumentoService'; // Adicionar esta importação DialogActions,
} 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;
@@ -27,7 +34,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;
@@ -40,11 +47,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([]);
@@ -54,9 +61,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]);
@@ -64,13 +71,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);
} }
@@ -80,36 +87,38 @@ 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(a => a.id === selectedAtendimento); const atendimento = atendimentos.find(
(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
@@ -117,65 +126,73 @@ 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', { month: 'long' }).format(new Date()), mesExtenso: new Intl.DateTimeFormat("pt-BR", {
ano: new Date().getFullYear().toString() month: "long",
} }).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(/\{\{([^}]+)\}\}/g, (match, path) => { const conteudoPreenchido = template.conteudo.replace(
/\{\{([^}]+)\}\}/g,
(match, path) => {
try { try {
const valor = path.split('.').reduce((obj, key) => obj?.[key], variables); const valor = path
.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('Erro ao carregar template. Verifique se o atendimento possui um cliente vinculado.'); toast.error(
"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;
} }
@@ -186,12 +203,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);
} }
@@ -203,12 +220,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>
@@ -216,12 +233,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>
@@ -259,23 +276,39 @@ export function DocumentoEditor({
height: 500, height: 500,
menubar: true, menubar: true,
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: 'undo redo | formatselect | ' + toolbar:
'bold italic backcolor | alignleft aligncenter ' + "undo redo | formatselect | " +
'alignright alignjustify | bullist numlist outdent indent | ' + "bold italic backcolor | alignleft aligncenter " +
'removeformat | help', "alignright alignjustify | bullist numlist outdent indent | " +
language: 'pt_BR', // Adicionar configuração de idioma "removeformat | help",
language_url: '/tinymce/langs/pt_BR.js', // Adicionar URL do arquivo de idioma language: "pt_BR", // Adicionar configuração de idioma
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }', 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 }",
// 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}
@@ -298,4 +331,3 @@ export function DocumentoEditor({
</Dialog> </Dialog>
); );
} }
+74 -44
View File
@@ -1,8 +1,13 @@
import { Dialog } from '@headlessui/react'; import { Dialog } from "@headlessui/react";
import { XMarkIcon, PrinterIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline'; import {
import { api } from '../../lib/api'; XMarkIcon,
import { toast } from 'react-toastify'; PrinterIcon,
import { useState } from 'react'; DocumentArrowDownIcon,
} 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;
@@ -10,18 +15,22 @@ interface DocumentoViewerProps {
onClose: () => void; onClose: () => void;
} }
export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerProps) { export function DocumentoViewer({
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;
@@ -88,16 +97,16 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
</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>
@@ -117,25 +126,32 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
const handleExportPDF = async () => { const handleExportPDF = async () => {
try { try {
setIsExporting(true); setIsExporting(true);
toast.info('Gerando PDF, aguarde...', { autoClose: false, toastId: 'generating-pdf' }); toast.info("Gerando PDF, aguarde...", {
autoClose: false,
// Buscar configurações atualizadas do escritório toastId: "generating-pdf",
const response = await api.post('/documentos/exportar-pdf', {
conteudo: documento.conteudo,
escritorio: documento.escritorio
}, {
responseType: 'blob',
headers: {
'Accept': 'application/pdf'
}
}); });
// Buscar configurações atualizadas do escritório
const response = await api.post(
"/documentos/exportar-pdf",
{
conteudo: documento.conteudo,
escritorio: documento.escritorio,
},
{
responseType: "blob",
headers: {
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();
@@ -143,23 +159,19 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
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 <Dialog open={isOpen} onClose={onClose} className="relative z-50">
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">
@@ -174,15 +186,31 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
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 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"> <svg
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
<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> 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> </svg>
Gerando PDF... Gerando PDF...
</> </>
@@ -208,7 +236,7 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
<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>
@@ -218,8 +246,10 @@ export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerP
/> />
<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}<br/> {documento?.escritorio?.endereco}
{documento?.escritorio?.telefone} | {documento?.escritorio?.email} <br />
{documento?.escritorio?.telefone} |{" "}
{documento?.escritorio?.email}
</div> </div>
</div> </div>
</div> </div>
+62 -44
View File
@@ -1,13 +1,21 @@
import { useState, useEffect, useCallback } from 'react'; import { Dialog } from "@headlessui/react";
import { useParams, useNavigate, Link } from 'react-router-dom'; import {
import { DocumentIcon, PlusIcon, TrashIcon, EyeIcon, CogIcon } from '@heroicons/react/24/outline'; DocumentIcon,
import { DocumentoEditor } from './DocumentoEditor'; PlusIcon,
import { api } from '../lib/api'; TrashIcon,
import { toast } from 'react-toastify'; EyeIcon,
import { useAuth } from '../contexts/AuthContext'; CogIcon,
import { Dialog } from '@headlessui/react'; XMarkIcon,
import { XMarkIcon } from '@heroicons/react/24/outline'; } from "@heroicons/react/24/outline";
import { DocumentoViewer } from './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 "./DocumentoEditor";
import { DocumentoViewer } from "./DocumentoViewer";
export function DocumentosPage() { export function DocumentosPage() {
const [isEditorOpen, setIsEditorOpen] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false);
@@ -23,7 +31,7 @@ export function DocumentosPage() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!user) { if (!user) {
navigate('/login'); navigate("/login");
return; return;
} }
@@ -31,12 +39,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);
@@ -52,12 +60,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);
}; };
@@ -66,16 +74,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");
} }
} }
}; };
@@ -83,30 +91,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");
} }
}; };
@@ -126,17 +134,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" />
@@ -153,8 +161,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
@@ -163,8 +171,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
@@ -175,11 +183,16 @@ 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 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
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">{doc.nome}</h3> <h3 className="font-semibold text-gray-900 dark:text-gray-100">
{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()}
@@ -209,16 +222,21 @@ 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 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
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">{modelo.nome}</h3> <h3 className="font-semibold text-gray-900 dark:text-gray-100">
{modelo.nome}
</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{modelo.descricao}</p> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<button {modelo.descricao}
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" </p>
> <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>
+107 -61
View File
@@ -1,10 +1,16 @@
import { useState, useEffect } from 'react'; import { Dialog } from "@headlessui/react";
import { api } from '../../lib/api'; import {
import { Editor } from '@tinymce/tinymce-react'; DocumentIcon,
import { toast } from 'react-toastify'; PencilIcon,
import { Dialog } from '@headlessui/react'; TrashIcon,
import { DocumentIcon, PencilIcon, TrashIcon, PlusIcon } from '@heroicons/react/24/outline'; PlusIcon,
import { DOCUMENT_VARIABLES } from '../../utils/documentVariables'; } from "@heroicons/react/24/outline";
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([]);
@@ -12,19 +18,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");
} }
}; };
@@ -36,15 +42,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");
} }
}; };
@@ -55,35 +61,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");
} }
}; };
@@ -98,7 +104,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);
}; };
@@ -110,11 +116,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);
}} }}
@@ -127,12 +133,17 @@ 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 key={template.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4"> <div
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">{template.descricao}</p> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{template.descricao}
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => handleEdit(template)} onClick={() => handleEdit(template)}
@@ -154,14 +165,18 @@ export function GerenciarTemplates() {
</div> </div>
{/* Modal de Edição */} {/* Modal de Edição */}
<Dialog open={isEditorModalOpen} onClose={() => setEditorModalOpen(false)} className="relative z-50"> <Dialog
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">
@@ -170,7 +185,9 @@ export function GerenciarTemplates() {
<input <input
type="text" type="text"
value={formData.nome} value={formData.nome}
onChange={(e) => setFormData({...formData, nome: e.target.value})} onChange={(e) =>
setFormData({ ...formData, nome: e.target.value })
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
required required
/> />
@@ -180,7 +197,9 @@ 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) => setFormData({...formData, tipo: e.target.value})} onChange={(e) =>
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>
@@ -195,7 +214,9 @@ 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) => setFormData({...formData, descricao: e.target.value})} onChange={(e) =>
setFormData({ ...formData, descricao: e.target.value })
}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
rows={3} rows={3}
/> />
@@ -204,21 +225,27 @@ 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(([category, variables]) => ( {Object.entries(DOCUMENT_VARIABLES).map(
([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">{category}</h4> <h4 className="font-bold text-sm text-gray-700">
{Object.entries(variables).map(([name, variable]) => ( {category}
</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>
@@ -227,31 +254,50 @@ 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) => setFormData({...formData, conteudo: content})} onEditorChange={(content) =>
setFormData({ ...formData, conteudo: content })
}
init={{ init={{
height: 400, height: 400,
menubar: true, menubar: true,
plugins: [ plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', "advlist",
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', "autolink",
'insertdatetime', 'media', 'table', 'help', 'wordcount' "lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"help",
"wordcount",
], ],
toolbar: 'variables | undo redo | formatselect | bold italic | alignleft aligncenter alignright', toolbar:
"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(DOCUMENT_VARIABLES).flatMap(([category, vars]) => const items = Object.entries(
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>
+11 -12
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from "react";
import { DocumentoService } from '../../services/DocumentoService';
import { TIPOS_DOCUMENTO } from '../../utils/documentTemplates'; import { DocumentoService } from "../../services/DocumentoService";
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);
@@ -8,24 +9,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
@@ -34,15 +35,13 @@ 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}> <button onClick={handleGerar}>Gerar Documento</button>
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";
+10 -4
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,9 +52,15 @@ export const FormSelect = forwardRef<HTMLSelectElement, any>(
`} `}
{...props} {...props}
> >
<option value="" className="dark:bg-gray-700">Selecione...</option> <option value="" className="dark:bg-gray-700">
Selecione...
</option>
{options?.map((opt: any) => ( {options?.map((opt: any) => (
<option key={opt.value} value={opt.value} className="dark:bg-gray-700"> <option
key={opt.value}
value={opt.value}
className="dark:bg-gray-700"
>
{opt.label} {opt.label}
</option> </option>
))} ))}
+63 -50
View File
@@ -1,6 +1,4 @@
import React, { useState, Fragment, useEffect, useRef } from "react"; import { Menu, Transition } from "@headlessui/react";
import { Outlet, Link, useLocation } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
import { import {
BellIcon, BellIcon,
ChevronDownIcon, ChevronDownIcon,
@@ -18,15 +16,18 @@ import {
CurrencyDollarIcon, CurrencyDollarIcon,
CalculatorIcon, CalculatorIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { Menu, Transition } from "@headlessui/react"; import CalculateIcon from "@mui/icons-material/Calculate";
import api from "../../services/api"; 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 { useTheme } from "../../contexts/ThemeContext";
import api from "../../services/api";
// 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);
@@ -66,18 +67,18 @@ export const MainLayout: React.FC = () => {
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 },
@@ -87,28 +88,28 @@ export const MainLayout: React.FC = () => {
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
@@ -128,7 +129,10 @@ 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('--menu-item-top', `${rect.top}px`); document.documentElement.style.setProperty(
"--menu-item-top",
`${rect.top}px`,
);
} }
}; };
@@ -169,10 +173,7 @@ 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 <div key={item.path || item.label} className="px-2 mb-2">
key={item.path || item.label}
className="px-2 mb-2"
>
{!item.subitems ? ( {!item.subitems ? (
<Link <Link
to={item.path} to={item.path}
@@ -184,7 +185,9 @@ 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">{item.label}</span> <span className="text-xs mt-1 text-white/80">
{item.label}
</span>
</Link> </Link>
) : ( ) : (
<div <div
@@ -195,14 +198,18 @@ 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 => isCurrentPath(subitem.path)) item.subitems.some((subitem) =>
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">{item.label}</span> <span className="text-xs mt-1 text-white/80">
{item.label}
</span>
</div> </div>
</div> </div>
)} )}
@@ -213,24 +220,26 @@ 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(subitem => .filter(
(subitem) =>
!subitem.roles?.length || !subitem.roles?.length ||
(userProfile?.perfil && subitem.roles.includes(userProfile.perfil)) (userProfile?.perfil &&
subitem.roles.includes(userProfile.perfil)),
) )
.map((subitem) => ( .map((subitem) => (
<Link <Link
@@ -242,7 +251,9 @@ 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">{subitem.label}</span> <span className="text-sm text-white/90">
{subitem.label}
</span>
</div> </div>
</Link> </Link>
))} ))}
@@ -295,7 +306,9 @@ 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">{user?.cargo}</div> <div className="text-xs text-gray-500 dark:text-gray-400">
{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>
+2 -1
View File
@@ -1,7 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { Usuario } from "../../types/usuario";
import api from "../../services/api"; import api from "../../services/api";
import { Usuario } from "../../types/usuario";
import { UF_BRASIL } from "../../utils/constants/estados"; import { UF_BRASIL } from "../../utils/constants/estados";
export function PerfilPage() { export function PerfilPage() {
+4 -3
View File
@@ -1,11 +1,12 @@
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 { formatCurrency, formatDate } from "../../utils/format"; import { useState, lazy, Suspense, useEffect } from "react";
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(() =>
+2 -1
View File
@@ -1,7 +1,8 @@
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,8 +1,9 @@
import { useProcessoContext } from "../../context/ProcessoContext"; import { ClockIcon, DocumentIcon } from "@heroicons/react/24/outline";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
import { format } from "date-fns"; import { format } from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { ClockIcon, DocumentIcon } from "@heroicons/react/24/outline";
import { useProcessoContext } from "../../context/ProcessoContext";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
export default function AbaMovimentacoes() { export default function AbaMovimentacoes() {
const { processo } = useProcessoContext(); const { processo } = useProcessoContext();
+2 -1
View File
@@ -1,6 +1,7 @@
import React from "react";
import { Dialog } from "@headlessui/react"; import { Dialog } from "@headlessui/react";
import React from "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 { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 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 { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR"; 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";
interface DialogCustaProcessualProps { interface DialogCustaProcessualProps {
open: boolean; open: boolean;
+4 -3
View File
@@ -9,12 +9,13 @@ import {
Select, Select,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 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 { 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 ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react";
import { IHonorario, IHonorarioCreate } from "../types/financeiro"; import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps { interface DialogHonorarioProps {
@@ -1,5 +1,6 @@
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,8 +1,9 @@
import React, { useState } from "react";
import { Dialog } from "@headlessui/react"; import { Dialog } from "@headlessui/react";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
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;
+22 -16
View File
@@ -1,24 +1,25 @@
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 }) => {
@@ -40,16 +41,21 @@ 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', { style: 'currency', currency: 'BRL' })} {processo.valorCausa?.toLocaleString("pt-BR", {
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 className={`px-2 py-1 text-xs rounded-full ${resultadoColor[processo.resultado]}`}> <span
className={`px-2 py-1 text-xs rounded-full ${resultadoColor[processo.resultado]}`}
>
{resultadoLabel[processo.resultado]} {resultadoLabel[processo.resultado]}
</span> </span>
</div> </div>
+21 -17
View File
@@ -1,10 +1,6 @@
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,
@@ -15,14 +11,20 @@ import {
ExclamationTriangleIcon, ExclamationTriangleIcon,
PlusIcon, PlusIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ProcessoModal } from "./ProcessoModal.tsx"; import React, { useState, useEffect, lazy, Suspense } from "react";
import { formatNumeroProcesso, formatCPF } from "../../utils/formatters"; import { useParams, useNavigate } from "react-router-dom";
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"; 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";
// Adicionar lazy import para AbaMovimentacoes // Adicionar lazy import para AbaMovimentacoes
const AbaMovimentacoes = lazy(() => import("./AbaMovimentacoes")); const AbaMovimentacoes = lazy(() => import("./AbaMovimentacoes"));
@@ -192,25 +194,27 @@ 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">Parcialmente Procedente</option> <option value="PARCIALMENTE_PROCEDENTE">
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>
+22 -18
View File
@@ -1,6 +1,7 @@
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;
@@ -8,14 +9,17 @@ 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> = ({ onSubmit, initialData }) => { export const ProcessoForm: React.FC<ProcessoFormProps> = ({
onSubmit,
initialData,
}) => {
const { register, handleSubmit, reset } = useForm<Partial<Processo>>({ const { register, handleSubmit, reset } = useForm<Partial<Processo>>({
defaultValues: initialData, defaultValues: initialData,
}); });
@@ -28,7 +32,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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>
@@ -38,7 +42,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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>
@@ -58,7 +62,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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>
@@ -75,10 +79,10 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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>
@@ -93,7 +97,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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>
@@ -104,7 +108,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
</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>
@@ -116,7 +120,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
Ó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>
@@ -126,7 +130,7 @@ export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialDat
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,5 +1,6 @@
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 {
+11 -9
View File
@@ -1,20 +1,22 @@
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 { useForm } from "react-hook-form";
import { Processo, NaturezaProcesso, TipoParte } from "../../types/entities";
import { useProcessos } from "../../hooks/useProcessos";
import { useClientes } from "../../hooks/useClientes";
import { FormInput, FormSelect, FormCheckbox } from "../forms";
import axios from "axios"; import axios from "axios";
import { UF_BRASIL, TRIBUNAIS } from "../../utils/constants/tribunais"; import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { useClientes } from "../../hooks/useClientes";
import { useDatajud } from "../../hooks/useDatajud"; import { useDatajud } from "../../hooks/useDatajud";
import { ProcessoInfo } from "./ProcessoInfo"; // Adicionar import import { useProcessos } from "../../hooks/useProcessos";
import { DatajudProcesso } from "../../types/datajud"; import { DatajudProcesso } from "../../types/datajud";
import { Processo, NaturezaProcesso, TipoParte } from "../../types/entities";
import { UF_BRASIL, TRIBUNAIS } from "../../utils/constants/tribunais";
import { FormInput, FormSelect, FormCheckbox } from "../forms";
import { ProcessoInfo } from "./ProcessoInfo"; // Adicionar import
interface ProcessoModalProps { interface ProcessoModalProps {
isOpen: boolean; isOpen: boolean;
@@ -140,7 +142,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);
+79 -42
View File
@@ -1,43 +1,51 @@
import React from 'react'; import { format } from "date-fns";
import { useQuery } from 'react-query'; import { ptBR } from "date-fns/locale";
import { useParams } from 'react-router-dom'; import React from "react";
import api from '../../services/api'; import { useQuery } from "react-query";
import { format } from 'date-fns'; import { useParams } from "react-router-dom";
import { ptBR } from 'date-fns/locale';
import { Processo } from '../../types/entities'; import api from "../../services/api";
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': return 'bg-green-100 text-green-800'; case "PROCEDENTE":
case 'PARCIALMENTE_PROCEDENTE': return 'bg-yellow-100 text-yellow-800'; return "bg-green-100 text-green-800";
case 'IMPROCEDENTE': return 'bg-red-100 text-red-800'; case "PARCIALMENTE_PROCEDENTE":
case 'SEM_ANALISE_MERITO': return 'bg-gray-100 text-gray-800'; return "bg-yellow-100 text-yellow-800";
default: return 'bg-blue-100 text-blue-800'; case "IMPROCEDENTE":
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 { data: processo, refetch, isLoading, isError } = useQuery<Processo>( const {
['processo', id], data: processo,
async () => { refetch,
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>;
@@ -52,7 +60,9 @@ 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">Status do Processo</h3> <h3 className="text-lg font-medium text-gray-900 dark:text-white">
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">
@@ -62,17 +72,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>
@@ -90,8 +100,12 @@ 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">Resultado:</span> <span className="text-sm font-medium text-gray-500">
<span className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}> 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>
@@ -106,31 +120,52 @@ 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">{processo.cliente.nome}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{processo.nomeContraparte}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{processo.natureza}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{processo.status}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{processo.valorCausa?.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{format(new Date(processo.dataCriacao), 'dd/MM/yyyy', { locale: ptBR })}</p> <p className="text-gray-700 dark:text-gray-300">
{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">{format(new Date(processo.dataAtualizacao), 'dd/MM/yyyy', { locale: ptBR })}</p> <p className="text-gray-700 dark:text-gray-300">
{format(new Date(processo.dataAtualizacao), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -138,7 +173,9 @@ 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 className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}> <span
className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}
>
{getResultadoLabel(processo.resultado)} {getResultadoLabel(processo.resultado)}
</span> </span>
<select <select
@@ -146,17 +183,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>
+24 -15
View File
@@ -1,9 +1,10 @@
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 { Processo } from "../../types/entities";
import { useProcessos } from "../../hooks/useProcessos"; import { useProcessos } from "../../hooks/useProcessos";
import { Processo } from "../../types/entities";
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;
@@ -11,23 +12,26 @@ 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 resultadoColor[resultado as keyof typeof resultadoColor] || 'bg-gray-100 text-gray-800'; return (
resultadoColor[resultado as keyof typeof resultadoColor] ||
"bg-gray-100 text-gray-800"
);
}; };
export const ProcessosList: React.FC<ProcessosListProps> = ({ export const ProcessosList: React.FC<ProcessosListProps> = ({
@@ -46,7 +50,10 @@ export const ProcessosList: React.FC<ProcessosListProps> = ({
); );
const getResultadoDisplay = (processo: Processo) => { const getResultadoDisplay = (processo: Processo) => {
if (!processo.resultado || !resultadoLabel[processo.resultado as keyof typeof resultadoLabel]) { if (
!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
@@ -55,7 +62,9 @@ export const ProcessosList: React.FC<ProcessosListProps> = ({
} }
return ( return (
<span className={`px-2 py-1 text-xs rounded-full ${getResultadoColor(processo.resultado)}`}> <span
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,9 +1,11 @@
import React, { useState } from "react";
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { ProcessosList } from "./ProcessosList"; import React, { useState } from "react";
import { ProcessoModal } from "./ProcessoModal";
import { Processo } from "../../types/entities"; import { Processo } from "../../types/entities";
import { ProcessoModal } from "./ProcessoModal";
import { ProcessosList } from "./ProcessosList";
export const ProcessosPage: React.FC = () => { export const ProcessosPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -43,9 +45,7 @@ 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> <tr>{/* ...headers... */}</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 { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 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 { 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 ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react";
interface DialogCustaProcessualProps { interface DialogCustaProcessualProps {
open: boolean; open: boolean;
@@ -9,12 +9,13 @@ import {
Select, Select,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; 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 { 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 ptBR from "date-fns/locale/pt-BR";
import { useState, useEffect } from "react";
import { IHonorario, IHonorarioCreate } from "../types/financeiro"; import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps { interface DialogHonorarioProps {
@@ -1,12 +1,13 @@
import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "@heroicons/react/24/outline";
import { DatePicker } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import ptBR from "date-fns/locale/pt-BR";
import { formatCurrency } from "../../../utils/format";
import { StatusPagamento } from "@prisma/client"; import { StatusPagamento } from "@prisma/client";
import ptBR from "date-fns/locale/pt-BR";
import { Fragment, useState } from "react";
import { formatCurrency } from "../../../utils/format";
interface ParcelaHonorario { interface ParcelaHonorario {
id: string; id: string;
@@ -1,4 +1,4 @@
import { memo } from "react"; import { Edit as EditIcon } from "@mui/icons-material";
import { import {
Table, Table,
TableBody, TableBody,
@@ -8,7 +8,8 @@ import {
Chip, Chip,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { Edit as EditIcon } from "@mui/icons-material"; import { memo } from "react";
import { formatCurrency, formatDate } from "../../../utils/format"; import { formatCurrency, formatDate } from "../../../utils/format";
const CustasTable = memo(({ custas, onEdit, getStatusColor }) => { const CustasTable = memo(({ custas, onEdit, getStatusColor }) => {
@@ -1,31 +1,32 @@
import { useState } from 'react'; import { Dialog } from "@headlessui/react";
import { useQuery, useQueryClient } from 'react-query'; import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import { formatCurrency } from '../../utils/format'; import { useState } from "react";
import { Dialog } from '@headlessui/react'; import { useForm } from "react-hook-form";
import { useForm } from 'react-hook-form'; import { useQuery, useQueryClient } from "react-query";
import { FormInput, FormSelect } from '../forms';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import api from "../../services/api";
import api from '../../services/api'; import { formatCurrency } from "../../utils/format";
import { FormInput, FormSelect } from "../forms";
// Função auxiliar para retornar as cores do status // Função auxiliar para retornar as cores do status
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const colors = { const colors = {
PENDENTE: 'bg-yellow-100 text-yellow-800', PENDENTE: "bg-yellow-100 text-yellow-800",
PAGO: 'bg-green-100 text-green-800', PAGO: "bg-green-100 text-green-800",
VENCIDO: 'bg-red-100 text-red-800', VENCIDO: "bg-red-100 text-red-800",
CANCELADO: 'bg-gray-100 text-gray-800', CANCELADO: "bg-gray-100 text-gray-800",
PARCIALMENTE_PAGO: 'bg-blue-100 text-blue-800' PARCIALMENTE_PAGO: "bg-blue-100 text-blue-800",
}; };
return colors[status] || colors.PENDENTE; return colors[status] || colors.PENDENTE;
}; };
const TIPOS_CUSTA = [ const TIPOS_CUSTA = [
{ value: 'TAXA_ADMINISTRATIVA', label: 'Taxa Administrativa' }, { value: "TAXA_ADMINISTRATIVA", label: "Taxa Administrativa" },
{ value: 'RECURSO', label: 'Recurso' }, { value: "RECURSO", label: "Recurso" },
{ value: 'DILIGENCIA', label: 'Diligência' }, { value: "DILIGENCIA", label: "Diligência" },
{ value: 'CERTIDAO', label: 'Certidão' }, { value: "CERTIDAO", label: "Certidão" },
{ value: 'COPIA', label: 'Cópia' }, { value: "COPIA", label: "Cópia" },
{ value: 'OUTROS', label: 'Outros' } { value: "OUTROS", label: "Outros" },
]; ];
export function CustaAdministrativaPanel({ processoId }) { export function CustaAdministrativaPanel({ processoId }) {
@@ -35,18 +36,23 @@ export function CustaAdministrativaPanel({ processoId }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: custas, isLoading } = useQuery( const { data: custas, isLoading } = useQuery(
['custas-administrativas', processoId], ["custas-administrativas", processoId],
async () => { async () => {
const response = await api.get(`/processos-administrativos/${processoId}/custas`); const response = await api.get(
`/processos-administrativos/${processoId}/custas`,
);
return response.data; return response.data;
} },
); );
const handleEdit = (custa) => { const handleEdit = (custa) => {
setSelectedCusta(custa); setSelectedCusta(custa);
Object.keys(custa).forEach(key => { Object.keys(custa).forEach((key) => {
if (key === 'vencimento' || key === 'dataPagamento') { if (key === "vencimento" || key === "dataPagamento") {
setValue(key, custa[key] ? new Date(custa[key]).toISOString().split('T')[0] : ''); setValue(
key,
custa[key] ? new Date(custa[key]).toISOString().split("T")[0] : "",
);
} else { } else {
setValue(key, custa[key]); setValue(key, custa[key]);
} }
@@ -55,12 +61,14 @@ export function CustaAdministrativaPanel({ processoId }) {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (window.confirm('Tem certeza que deseja excluir esta custa?')) { if (window.confirm("Tem certeza que deseja excluir esta custa?")) {
try { try {
await api.delete(`/processos-administrativos/${processoId}/custas/${id}`); await api.delete(
queryClient.invalidateQueries(['custas-administrativas', processoId]); `/processos-administrativos/${processoId}/custas/${id}`,
);
queryClient.invalidateQueries(["custas-administrativas", processoId]);
} catch (error) { } catch (error) {
console.error('Erro ao excluir custa:', error); console.error("Erro ao excluir custa:", error);
} }
} }
}; };
@@ -70,23 +78,33 @@ export function CustaAdministrativaPanel({ processoId }) {
// Formatar as datas antes de enviar // Formatar as datas antes de enviar
const formattedData = { const formattedData = {
...data, ...data,
vencimento: data.vencimento ? new Date(data.vencimento).toISOString() : null, vencimento: data.vencimento
dataPagamento: data.dataPagamento ? new Date(data.dataPagamento).toISOString() : null, ? new Date(data.vencimento).toISOString()
valor: Number(data.valor) : null,
dataPagamento: data.dataPagamento
? new Date(data.dataPagamento).toISOString()
: null,
valor: Number(data.valor),
}; };
if (selectedCusta) { if (selectedCusta) {
await api.put(`/processos-administrativos/${processoId}/custas/${selectedCusta.id}`, formattedData); await api.put(
`/processos-administrativos/${processoId}/custas/${selectedCusta.id}`,
formattedData,
);
} else { } else {
await api.post(`/processos-administrativos/${processoId}/custas`, formattedData); await api.post(
`/processos-administrativos/${processoId}/custas`,
formattedData,
);
} }
queryClient.invalidateQueries(['custas-administrativas', processoId]); queryClient.invalidateQueries(["custas-administrativas", processoId]);
setIsModalOpen(false); setIsModalOpen(false);
setSelectedCusta(null); setSelectedCusta(null);
reset(); reset();
} catch (error) { } catch (error) {
console.error('Erro ao salvar custa:', error); console.error("Erro ao salvar custa:", error);
} }
}; };
@@ -108,27 +126,49 @@ export function CustaAdministrativaPanel({ processoId }) {
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Tipo</th> <th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Descrição</th> Tipo
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Valor</th> </th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th> <th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Vencimento</th> Descrição
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Ações</th> </th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Valor
</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Vencimento
</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Ações
</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 bg-white"> <tbody className="divide-y divide-gray-200 bg-white">
{custas?.map((custa) => ( {custas?.map((custa) => (
<tr key={custa.id}> <tr key={custa.id}>
<td className="px-3 py-4 text-sm text-gray-900">{custa.tipo}</td> <td className="px-3 py-4 text-sm text-gray-900">
<td className="px-3 py-4 text-sm text-gray-500">{custa.descricao}</td> {custa.tipo}
<td className="px-3 py-4 text-sm text-gray-900">{formatCurrency(custa.valor)}</td> </td>
<td className="px-3 py-4 text-sm text-gray-500">
{custa.descricao}
</td>
<td className="px-3 py-4 text-sm text-gray-900">
{formatCurrency(custa.valor)}
</td>
<td className="px-3 py-4 text-sm"> <td className="px-3 py-4 text-sm">
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(custa.status)}`}> <span
className={`px-2 py-1 rounded-full text-xs ${getStatusColor(custa.status)}`}
>
{custa.status} {custa.status}
</span> </span>
</td> </td>
<td className="px-3 py-4 text-sm text-gray-500"> <td className="px-3 py-4 text-sm text-gray-500">
{custa.vencimento ? new Date(custa.vencimento).toLocaleDateString() : '-'} {custa.vencimento
? new Date(custa.vencimento).toLocaleDateString()
: "-"}
</td> </td>
<td className="px-3 py-4 text-sm"> <td className="px-3 py-4 text-sm">
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -153,17 +193,20 @@ export function CustaAdministrativaPanel({ processoId }) {
</div> </div>
{/* Modal de Custa */} {/* Modal de Custa */}
<Dialog open={isModalOpen} onClose={() => { <Dialog
open={isModalOpen}
onClose={() => {
setIsModalOpen(false); setIsModalOpen(false);
setSelectedCusta(null); setSelectedCusta(null);
reset(); reset();
}}> }}
>
<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">
<Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6"> <Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6">
<Dialog.Title className="text-lg font-semibold mb-4"> <Dialog.Title className="text-lg font-semibold mb-4">
{selectedCusta ? 'Editar Custa' : 'Nova Custa'} {selectedCusta ? "Editar Custa" : "Nova Custa"}
</Dialog.Title> </Dialog.Title>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
@@ -195,10 +238,10 @@ export function CustaAdministrativaPanel({ processoId }) {
label="Status" label="Status"
{...register("status")} {...register("status")}
options={[ options={[
{ value: 'PENDENTE', label: 'Pendente' }, { value: "PENDENTE", label: "Pendente" },
{ value: 'PAGO', label: 'Pago' }, { value: "PAGO", label: "Pago" },
{ value: 'VENCIDO', label: 'Vencido' }, { value: "VENCIDO", label: "Vencido" },
{ value: 'CANCELADO', label: 'Cancelado' } { value: "CANCELADO", label: "Cancelado" },
]} ]}
/> />
@@ -1,7 +1,8 @@
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from "@headlessui/react";
import { EllipsisVerticalIcon } from '@heroicons/react/24/outline'; import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
import { Fragment } from 'react'; import { Fragment } from "react";
import { MovimentacaoAdministrativa } from '../../types/processoAdministrativo';
import { MovimentacaoAdministrativa } from "../../types/processoAdministrativo";
interface MovimentacaoAdminActionsProps { interface MovimentacaoAdminActionsProps {
movimentacao: MovimentacaoAdministrativa; movimentacao: MovimentacaoAdministrativa;
@@ -9,7 +10,11 @@ interface MovimentacaoAdminActionsProps {
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
export function MovimentacaoAdminActions({ movimentacao, onEdit, onDelete }: MovimentacaoAdminActionsProps) { export function MovimentacaoAdminActions({
movimentacao,
onEdit,
onDelete,
}: MovimentacaoAdminActionsProps) {
return ( return (
<Menu as="div" className="relative"> <Menu as="div" className="relative">
<Menu.Button className="p-1 rounded-full hover:bg-gray-100"> <Menu.Button className="p-1 rounded-full hover:bg-gray-100">
@@ -31,7 +36,7 @@ export function MovimentacaoAdminActions({ movimentacao, onEdit, onDelete }: Mov
<button <button
onClick={() => onEdit(movimentacao)} onClick={() => onEdit(movimentacao)}
className={`${ className={`${
active ? 'bg-gray-100' : '' active ? "bg-gray-100" : ""
} block px-4 py-2 text-sm text-gray-700 w-full text-left`} } block px-4 py-2 text-sm text-gray-700 w-full text-left`}
> >
Editar Editar
@@ -43,7 +48,7 @@ export function MovimentacaoAdminActions({ movimentacao, onEdit, onDelete }: Mov
<button <button
onClick={() => onDelete(movimentacao.id)} onClick={() => onDelete(movimentacao.id)}
className={`${ className={`${
active ? 'bg-gray-100' : '' active ? "bg-gray-100" : ""
} block px-4 py-2 text-sm text-red-600 w-full text-left`} } block px-4 py-2 text-sm text-red-600 w-full text-left`}
> >
Excluir Excluir
@@ -1,5 +1,6 @@
import { Dialog } from "@headlessui/react"; import { Dialog } from "@headlessui/react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { MovimentacaoAdministrativa } from "../../types/processoAdministrativo"; import { MovimentacaoAdministrativa } from "../../types/processoAdministrativo";
interface MovimentacaoAdministrativaDialogProps { interface MovimentacaoAdministrativaDialogProps {
@@ -10,43 +11,43 @@ interface MovimentacaoAdministrativaDialogProps {
} }
const TIPOS_MOVIMENTACAO = [ const TIPOS_MOVIMENTACAO = [
'DESPACHO', "DESPACHO",
'DECISÃO', "DECISÃO",
'NOTIFICAÇÃO', "NOTIFICAÇÃO",
'MANIFESTAÇÃO', "MANIFESTAÇÃO",
'RECURSO', "RECURSO",
'OUTROS' "OUTROS",
] as const; ] as const;
export function MovimentacaoAdministrativaDialog({ export function MovimentacaoAdministrativaDialog({
isOpen, isOpen,
onClose, onClose,
onSave, onSave,
movimentacao movimentacao,
}: MovimentacaoAdministrativaDialogProps) { }: MovimentacaoAdministrativaDialogProps) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
data: new Date(), data: new Date(),
descricao: '', descricao: "",
tipoMovimentacao: '', tipoMovimentacao: "",
numero: '', numero: "",
documentos: [] as string[], documentos: [] as string[],
observacoes: '' observacoes: "",
}); });
useEffect(() => { useEffect(() => {
if (movimentacao) { if (movimentacao) {
setFormData({ setFormData({
...movimentacao, ...movimentacao,
data: new Date(movimentacao.data) data: new Date(movimentacao.data),
}); });
} else { } else {
setFormData({ setFormData({
data: new Date(), data: new Date(),
descricao: '', descricao: "",
tipoMovimentacao: '', tipoMovimentacao: "",
numero: '', numero: "",
documentos: [], documentos: [],
observacoes: '' observacoes: "",
}); });
} }
}, [movimentacao]); }, [movimentacao]);
@@ -60,19 +61,19 @@ export function MovimentacaoAdministrativaDialog({
try { try {
// Validações // Validações
if (!formData.descricao) { if (!formData.descricao) {
setFormError('A descrição é obrigatória'); setFormError("A descrição é obrigatória");
return; return;
} }
if (!formData.tipoMovimentacao) { if (!formData.tipoMovimentacao) {
setFormError('O tipo de movimentação é obrigatório'); setFormError("O tipo de movimentação é obrigatório");
return; return;
} }
await onSave(formData); await onSave(formData);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Erro ao salvar movimentação:', error); console.error("Erro ao salvar movimentação:", error);
setFormError('Erro ao salvar movimentação. Tente novamente.'); setFormError("Erro ao salvar movimentação. Tente novamente.");
} }
}; };
@@ -96,10 +97,12 @@ export function MovimentacaoAdministrativaDialog({
type="datetime-local" type="datetime-local"
className="w-full rounded-lg border-gray-300" className="w-full rounded-lg border-gray-300"
value={formData.data.toISOString().slice(0, 16)} value={formData.data.toISOString().slice(0, 16)}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
data: new Date(e.target.value) data: new Date(e.target.value),
})} })
}
required required
/> />
</div> </div>
@@ -111,10 +114,12 @@ export function MovimentacaoAdministrativaDialog({
<select <select
className="w-full rounded-lg border-gray-300" className="w-full rounded-lg border-gray-300"
value={formData.tipoMovimentacao} value={formData.tipoMovimentacao}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
tipoMovimentacao: e.target.value tipoMovimentacao: e.target.value,
})} })
}
required required
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
@@ -134,10 +139,12 @@ export function MovimentacaoAdministrativaDialog({
type="text" type="text"
className="w-full rounded-lg border-gray-300" className="w-full rounded-lg border-gray-300"
value={formData.numero} value={formData.numero}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
numero: e.target.value numero: e.target.value,
})} })
}
/> />
</div> </div>
@@ -149,10 +156,12 @@ export function MovimentacaoAdministrativaDialog({
className="w-full rounded-lg border-gray-300" className="w-full rounded-lg border-gray-300"
rows={3} rows={3}
value={formData.descricao} value={formData.descricao}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
descricao: e.target.value descricao: e.target.value,
})} })
}
required required
/> />
</div> </div>
@@ -165,10 +174,12 @@ export function MovimentacaoAdministrativaDialog({
className="w-full rounded-lg border-gray-300" className="w-full rounded-lg border-gray-300"
rows={2} rows={2}
value={formData.observacoes} value={formData.observacoes}
onChange={(e) => setFormData({ onChange={(e) =>
setFormData({
...formData, ...formData,
observacoes: e.target.value observacoes: e.target.value,
})} })
}
/> />
</div> </div>
</div> </div>
@@ -1,8 +1,4 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useQuery, useQueryClient } from "react-query";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { classNames } from "../../utils/classNames";
import { import {
ArrowLeftIcon, ArrowLeftIcon,
DocumentTextIcon, DocumentTextIcon,
@@ -11,13 +7,19 @@ import {
TrashIcon, TrashIcon,
CurrencyDollarIcon, CurrencyDollarIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { formatCurrency } from "../../utils/format"; import { useState } from "react";
import { useQuery, useQueryClient } from "react-query";
import { useParams, useNavigate } from "react-router-dom";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos"; import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { ProcessoAdminModal } from "./ProcessoAdminModal";
import { MovimentacaoAdministrativaDialog } from "./MovimentacaoAdministrativaDialog";
import { MovimentacaoAdminActions } from "./MovimentacaoAdminActions"; // Adicionar esta importação
import api from "../../services/api"; import api from "../../services/api";
import { classNames } from "../../utils/classNames";
import { formatCurrency } from "../../utils/format";
import { CustaAdministrativaPanel } from "./CustaAdministrativaPanel"; import { CustaAdministrativaPanel } from "./CustaAdministrativaPanel";
import { MovimentacaoAdminActions } from "./MovimentacaoAdminActions"; // Adicionar esta importação
import { MovimentacaoAdministrativaDialog } from "./MovimentacaoAdministrativaDialog";
import { ProcessoAdminModal } from "./ProcessoAdminModal";
const tabs = [ const tabs = [
{ id: "geral", name: "Informações Gerais", icon: DocumentTextIcon }, { id: "geral", name: "Informações Gerais", icon: DocumentTextIcon },
@@ -33,7 +35,8 @@ export function ProcessoAdminDetalhes() {
const [isMovimentacaoModalOpen, setIsMovimentacaoModalOpen] = useState(false); const [isMovimentacaoModalOpen, setIsMovimentacaoModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [movimentacaoParaEditar, setMovimentacaoParaEditar] = useState<MovimentacaoAdministrativa | null>(null); const [movimentacaoParaEditar, setMovimentacaoParaEditar] =
useState<MovimentacaoAdministrativa | null>(null);
// Se o id for "novo", renderizar diretamente o modal de criação // Se o id for "novo", renderizar diretamente o modal de criação
if (id === "novo") { if (id === "novo") {
@@ -46,7 +49,11 @@ export function ProcessoAdminDetalhes() {
); );
} }
const { data: processo, isLoading, error } = useQuery( const {
data: processo,
isLoading,
error,
} = useQuery(
["processo-administrativo", id], ["processo-administrativo", id],
async () => { async () => {
const response = await api.get(`/processos-administrativos/${id}`); const response = await api.get(`/processos-administrativos/${id}`);
@@ -57,8 +64,8 @@ export function ProcessoAdminDetalhes() {
retry: 2, // Tenta no máximo 2 vezes em caso de erro retry: 2, // Tenta no máximo 2 vezes em caso de erro
onError: (error) => { onError: (error) => {
console.error("Erro ao carregar processo:", error); console.error("Erro ao carregar processo:", error);
} },
} },
); );
const { deleteProcesso } = useProcessosAdministrativos(); const { deleteProcesso } = useProcessosAdministrativos();
@@ -85,7 +92,10 @@ export function ProcessoAdminDetalhes() {
const handleEditMovimentacao = async (data: any) => { const handleEditMovimentacao = async (data: any) => {
try { try {
// Atualizar URL para usar a nova rota // Atualizar URL para usar a nova rota
await api.put(`/processos-administrativos/movimentacoes/${data.id}`, data); await api.put(
`/processos-administrativos/movimentacoes/${data.id}`,
data,
);
queryClient.invalidateQueries(["processo-administrativo", id]); queryClient.invalidateQueries(["processo-administrativo", id]);
setMovimentacaoParaEditar(null); setMovimentacaoParaEditar(null);
} catch (error) { } catch (error) {
@@ -98,7 +108,9 @@ export function ProcessoAdminDetalhes() {
try { try {
// Atualizar URL para usar a nova rota // Atualizar URL para usar a nova rota
await api.delete(`/processos-administrativos/movimentacoes/${movimentacaoId}`); await api.delete(
`/processos-administrativos/movimentacoes/${movimentacaoId}`,
);
queryClient.invalidateQueries(["processo-administrativo", id]); queryClient.invalidateQueries(["processo-administrativo", id]);
} catch (error) { } catch (error) {
console.error("Erro ao excluir movimentação:", error); console.error("Erro ao excluir movimentação:", error);
@@ -158,7 +170,7 @@ export function ProcessoAdminDetalhes() {
"flex items-center justify-center", "flex items-center justify-center",
selected selected
? "bg-white text-blue-700 shadow" ? "bg-white text-blue-700 shadow"
: "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600" : "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600",
) )
} }
> >
@@ -176,7 +188,9 @@ export function ProcessoAdminDetalhes() {
<dt className="text-sm font-medium text-gray-500"> <dt className="text-sm font-medium text-gray-500">
Número/Protocolo Número/Protocolo
</dt> </dt>
<dd className="mt-1 text-sm text-gray-900">{processo.numero}</dd> <dd className="mt-1 text-sm text-gray-900">
{processo.numero}
</dd>
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-gray-500">Órgão</dt> <dt className="text-sm font-medium text-gray-500">Órgão</dt>
@@ -184,7 +198,9 @@ export function ProcessoAdminDetalhes() {
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-gray-500">Status</dt> <dt className="text-sm font-medium text-gray-500">Status</dt>
<dd className="mt-1 text-sm text-gray-900">{processo.status}</dd> <dd className="mt-1 text-sm text-gray-900">
{processo.status}
</dd>
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-gray-500"> <dt className="text-sm font-medium text-gray-500">
@@ -203,7 +219,9 @@ export function ProcessoAdminDetalhes() {
</dd> </dd>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<dt className="text-sm font-medium text-gray-500">Observações</dt> <dt className="text-sm font-medium text-gray-500">
Observações
</dt>
<dd className="mt-1 text-sm text-gray-900"> <dd className="mt-1 text-sm text-gray-900">
{processo.observacoes || "-"} {processo.observacoes || "-"}
</dd> </dd>
@@ -214,7 +232,9 @@ export function ProcessoAdminDetalhes() {
{/* Movimentações */} {/* Movimentações */}
<Tab.Panel className="bg-white rounded-xl p-6"> <Tab.Panel className="bg-white rounded-xl p-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Movimentações</h3> <h3 className="text-lg font-medium text-gray-900">
Movimentações
</h3>
<button <button
onClick={() => setIsMovimentacaoModalOpen(true)} onClick={() => setIsMovimentacaoModalOpen(true)}
className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg" className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
@@ -244,7 +264,9 @@ export function ProcessoAdminDetalhes() {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
<time>{new Date(mov.data).toLocaleDateString()}</time> <time>
{new Date(mov.data).toLocaleDateString()}
</time>
</div> </div>
<MovimentacaoAdminActions <MovimentacaoAdminActions
movimentacao={mov} movimentacao={mov}
@@ -1,11 +1,15 @@
import { Dialog } from "@headlessui/react"; import { Dialog } from "@headlessui/react";
import { useForm } from "react-hook-form";
import { ProcessoAdministrativo, StatusProcessoAdministrativo } from "../../types/processoAdministrativo";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { useClientes } from "../../hooks/useClientes";
import { FormInput, FormSelect } from "../forms";
import { TipoParte } from "../../types/entities";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { useClientes } from "../../hooks/useClientes";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { TipoParte } from "../../types/entities";
import {
ProcessoAdministrativo,
StatusProcessoAdministrativo,
} from "../../types/processoAdministrativo";
import { FormInput, FormSelect } from "../forms";
interface ProcessoAdminModalProps { interface ProcessoAdminModalProps {
isOpen: boolean; isOpen: boolean;
@@ -18,7 +22,7 @@ export function ProcessoAdminModal({
isOpen, isOpen,
onClose, onClose,
processo, processo,
onSuccess onSuccess,
}: ProcessoAdminModalProps) { }: ProcessoAdminModalProps) {
const { register, handleSubmit, setValue, reset } = useForm(); const { register, handleSubmit, setValue, reset } = useForm();
const { createProcesso, updateProcesso } = useProcessosAdministrativos(); const { createProcesso, updateProcesso } = useProcessosAdministrativos();
@@ -29,7 +33,11 @@ export function ProcessoAdminModal({
useEffect(() => { useEffect(() => {
if (isOpen && processo) { if (isOpen && processo) {
Object.keys(processo).forEach((key) => { Object.keys(processo).forEach((key) => {
if (key !== "id" && key !== "dataCriacao" && key !== "dataAtualizacao") { if (
key !== "id" &&
key !== "dataCriacao" &&
key !== "dataAtualizacao"
) {
setValue(key, processo[key]); setValue(key, processo[key]);
} }
}); });
@@ -52,13 +60,15 @@ export function ProcessoAdminModal({
console.log("Dados do formulário:", data); // Adicionar log console.log("Dados do formulário:", data); // Adicionar log
const processData = { const processData = {
...data, ...data,
valorPretensao: data.valorPretensao ? Number(data.valorPretensao) : null, valorPretensao: data.valorPretensao
? Number(data.valorPretensao)
: null,
}; };
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);
@@ -78,7 +88,9 @@ export function ProcessoAdminModal({
<div className="fixed inset-0 flex items-center justify-center p-4"> <div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6"> <Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6">
<Dialog.Title className="text-lg font-semibold mb-4"> <Dialog.Title className="text-lg font-semibold mb-4">
{processo ? "Editar Processo Administrativo" : "Novo Processo Administrativo"} {processo
? "Editar Processo Administrativo"
: "Novo Processo Administrativo"}
</Dialog.Title> </Dialog.Title>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
@@ -93,9 +105,9 @@ export function ProcessoAdminModal({
label="Cliente" label="Cliente"
required required
{...register("clienteId")} {...register("clienteId")}
options={clientes.map(c => ({ options={clientes.map((c) => ({
value: c.id, value: c.id,
label: `${c.nome} (${c.cpfCnpj})` label: `${c.nome} (${c.cpfCnpj})`,
}))} }))}
/> />
@@ -105,15 +117,11 @@ export function ProcessoAdminModal({
{...register("tipoParteCliente")} {...register("tipoParteCliente")}
options={[ options={[
{ value: TipoParte.AUTOR, label: "Autor" }, { value: TipoParte.AUTOR, label: "Autor" },
{ value: TipoParte.REU, label: "Réu" } { value: TipoParte.REU, label: "Réu" },
]} ]}
/> />
<FormInput <FormInput label="Órgão" required {...register("orgao")} />
label="Órgão"
required
{...register("orgao")}
/>
<div className="col-span-2"> <div className="col-span-2">
<FormInput <FormInput
@@ -136,10 +144,12 @@ export function ProcessoAdminModal({
label="Status" label="Status"
required required
{...register("status")} {...register("status")}
options={Object.values(StatusProcessoAdministrativo).map(status => ({ options={Object.values(StatusProcessoAdministrativo).map(
(status) => ({
value: status, value: status,
label: status.replace(/_/g, " ") label: status.replace(/_/g, " "),
}))} }),
)}
/> />
<div className="col-span-2"> <div className="col-span-2">
@@ -1,19 +1,23 @@
import { useNavigate } from "react-router-dom";
import { DocumentTextIcon } from "@heroicons/react/24/outline"; import { DocumentTextIcon } from "@heroicons/react/24/outline";
import { useNavigate } from "react-router-dom";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos"; import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import {
ProcessoAdministrativo,
StatusProcessoAdministrativo,
} from "../../types/processoAdministrativo";
import { formatCurrency } from "../../utils/format"; import { formatCurrency } from "../../utils/format";
import { ProcessoAdministrativo, StatusProcessoAdministrativo } from "../../types/processoAdministrativo";
const statusColor = { const statusColor = {
[StatusProcessoAdministrativo.INICIAL]: 'bg-yellow-100 text-yellow-800', [StatusProcessoAdministrativo.INICIAL]: "bg-yellow-100 text-yellow-800",
[StatusProcessoAdministrativo.EM_ANALISE]: 'bg-blue-100 text-blue-800', [StatusProcessoAdministrativo.EM_ANALISE]: "bg-blue-100 text-blue-800",
[StatusProcessoAdministrativo.EM_RECURSO]: 'bg-purple-100 text-purple-800', [StatusProcessoAdministrativo.EM_RECURSO]: "bg-purple-100 text-purple-800",
[StatusProcessoAdministrativo.FINALIZADO]: 'bg-green-100 text-green-800', [StatusProcessoAdministrativo.FINALIZADO]: "bg-green-100 text-green-800",
[StatusProcessoAdministrativo.ARQUIVADO]: 'bg-gray-100 text-gray-800' [StatusProcessoAdministrativo.ARQUIVADO]: "bg-gray-100 text-gray-800",
}; };
export function ProcessoAdministrativoList() { export function ProcessoAdministrativoList() {
console.log('Renderizando ProcessoAdministrativoList'); console.log("Renderizando ProcessoAdministrativoList");
const navigate = useNavigate(); const navigate = useNavigate();
const { processos, isLoading } = useProcessosAdministrativos(); const { processos, isLoading } = useProcessosAdministrativos();
@@ -25,7 +29,9 @@ export function ProcessoAdministrativoList() {
<div className="px-4 sm:px-6 lg:px-8"> <div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center"> <div className="sm:flex sm:items-center">
<div className="sm:flex-auto"> <div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Processos Administrativos</h1> <h1 className="text-xl font-semibold text-gray-900">
Processos Administrativos
</h1>
</div> </div>
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button <button
@@ -45,19 +51,34 @@ export function ProcessoAdministrativoList() {
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> <th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Número/Protocolo Número/Protocolo
</th> </th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> <th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Órgão Órgão
</th> </th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> <th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Status Status
</th> </th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"> <th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Valor Pretensão Valor Pretensão
</th> </th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6"> <th
scope="col"
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<span className="sr-only">Ações</span> <span className="sr-only">Ações</span>
</th> </th>
</tr> </tr>
@@ -72,16 +93,24 @@ export function ProcessoAdministrativoList() {
{processo.orgao} {processo.orgao}
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm"> <td className="whitespace-nowrap px-3 py-4 text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor[processo.status]}`}> <span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor[processo.status]}`}
>
{processo.status} {processo.status}
</span> </span>
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{processo.valorPretensao ? formatCurrency(processo.valorPretensao) : '-'} {processo.valorPretensao
? formatCurrency(processo.valorPretensao)
: "-"}
</td> </td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"> <td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<button <button
onClick={() => navigate(`/processos-administrativos/${processo.id}`)} onClick={() =>
navigate(
`/processos-administrativos/${processo.id}`,
)
}
className="text-blue-600 hover:text-blue-900" className="text-blue-600 hover:text-blue-900"
> >
Visualizar Visualizar
+41 -27
View File
@@ -1,25 +1,23 @@
import { useQuery } from 'react-query'; import { format } from "date-fns";
import { format } from 'date-fns'; import { useQuery } from "react-query";
import { Card } from '../../components/ui/Card';
import { Card } from "../../components/ui/Card";
//import { Prova } from '../../types/prova'; //import { Prova } from '../../types/prova';
export interface Prova { export interface Prova {
id: string; id: string;
fileName: string; fileName: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO'; status: "PENDENTE" | "PROCESSANDO" | "CONCLUIDO" | "ERRO";
dataCriacao: string; dataCriacao: string;
checksumSha256: string; checksumSha256: string;
error?: string; error?: string;
} }
export function AdminProvas() { export function AdminProvas() {
const { data: provas, isLoading } = useQuery<Prova[]>( const { data: provas, isLoading } = useQuery<Prova[]>("provas", async () => {
'provas', const response = await fetch("/api/provas/admin");
async () => {
const response = await fetch('/api/provas/admin');
return response.json(); return response.json();
} });
);
if (isLoading) return <div>Carregando...</div>; if (isLoading) return <div>Carregando...</div>;
@@ -28,9 +26,12 @@ export function AdminProvas() {
<Card> <Card>
<div className="sm:flex sm:items-center"> <div className="sm:flex sm:items-center">
<div className="sm:flex-auto"> <div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Documentos Validados</h1> <h1 className="text-xl font-semibold text-gray-900">
Documentos Validados
</h1>
<p className="mt-2 text-sm text-gray-700"> <p className="mt-2 text-sm text-gray-700">
Lista de todos os documentos processados pelo sistema de validação. Lista de todos os documentos processados pelo sistema de
validação.
</p> </p>
</div> </div>
</div> </div>
@@ -39,10 +40,18 @@ export function AdminProvas() {
<table className="min-w-full divide-y divide-gray-300"> <table className="min-w-full divide-y divide-gray-300">
<thead> <thead>
<tr> <tr>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Arquivo</th> <th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th> Arquivo
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Data</th> </th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Ações</th> <th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Data
</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Ações
</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 bg-white"> <tbody className="divide-y divide-gray-200 bg-white">
@@ -55,11 +64,11 @@ export function AdminProvas() {
<StatusBadge status={prova.status} /> <StatusBadge status={prova.status} />
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{format(new Date(prova.dataCriacao), 'dd/MM/yyyy HH:mm')} {format(new Date(prova.dataCriacao), "dd/MM/yyyy HH:mm")}
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm"> <td className="whitespace-nowrap px-3 py-4 text-sm">
<div className="flex space-x-2"> <div className="flex space-x-2">
{prova.status === 'CONCLUIDO' && ( {prova.status === "CONCLUIDO" && (
<> <>
<a <a
href={`/api/provas/relatorio/${prova.checksumSha256}`} href={`/api/provas/relatorio/${prova.checksumSha256}`}
@@ -79,12 +88,15 @@ export function AdminProvas() {
</a> </a>
</> </>
)} )}
{prova.status === 'ERRO' && ( {prova.status === "ERRO" && (
<span className="text-red-600"> <span className="text-red-600">
{prova.error ? prova.error.substring(0, 50) + "..." : "Erro desconhecido"} {prova.error
? prova.error.substring(0, 50) + "..."
: "Erro desconhecido"}
</span> </span>
)} )}
{(prova.status === 'PENDENTE' || prova.status === 'PROCESSANDO') && ( {(prova.status === "PENDENTE" ||
prova.status === "PROCESSANDO") && (
<span className="text-blue-600">Processando...</span> <span className="text-blue-600">Processando...</span>
)} )}
</div> </div>
@@ -99,16 +111,18 @@ export function AdminProvas() {
); );
} }
function StatusBadge({ status }: { status: Prova['status'] }) { function StatusBadge({ status }: { status: Prova["status"] }) {
const colors = { const colors = {
PENDENTE: 'bg-yellow-100 text-yellow-800', PENDENTE: "bg-yellow-100 text-yellow-800",
PROCESSANDO: 'bg-blue-100 text-blue-800', PROCESSANDO: "bg-blue-100 text-blue-800",
CONCLUIDO: 'bg-green-100 text-green-800', CONCLUIDO: "bg-green-100 text-green-800",
ERRO: 'bg-red-100 text-red-800' ERRO: "bg-red-100 text-red-800",
}; };
return ( return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[status]}`}> <span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[status]}`}
>
{status} {status}
</span> </span>
); );
+112 -75
View File
@@ -1,45 +1,49 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from "react";
import { useNavigate } from 'react-router-dom'; import { useDropzone } from "react-dropzone";
import { useDropzone } from 'react-dropzone'; import { useNavigate } from "react-router-dom";
import { toast } from 'react-toastify'; import { toast } from "react-toastify";
export function ClientProvaUpload() { export function ClientProvaUpload() {
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [userLocation, setUserLocation] = useState<{
const [locationPermission, setLocationPermission] = useState<string>('pending'); latitude: number;
longitude: number;
} | null>(null);
const [locationPermission, setLocationPermission] =
useState<string>("pending");
const [filePreview, setFilePreview] = useState<string | null>(null); const [filePreview, setFilePreview] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
// Tentar obter a localização do usuário quando o componente montar // Tentar obter a localização do usuário quando o componente montar
useEffect(() => { useEffect(() => {
if (navigator.geolocation) { if (navigator.geolocation) {
setLocationPermission('requesting'); setLocationPermission("requesting");
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(position) => { (position) => {
console.log('Localização obtida com sucesso'); console.log("Localização obtida com sucesso");
setUserLocation({ setUserLocation({
latitude: position.coords.latitude, latitude: position.coords.latitude,
longitude: position.coords.longitude longitude: position.coords.longitude,
}); });
setLocationPermission('granted'); setLocationPermission("granted");
}, },
(error) => { (error) => {
console.log('Erro ao obter localização:', error.message); console.log("Erro ao obter localização:", error.message);
setLocationPermission('denied'); setLocationPermission("denied");
}, },
{ {
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 5000, timeout: 5000,
maximumAge: 0 maximumAge: 0,
} },
); );
} else { } else {
console.log('Geolocalização não suportada pelo navegador'); console.log("Geolocalização não suportada pelo navegador");
setLocationPermission('unsupported'); setLocationPermission("unsupported");
} }
}, []); }, []);
@@ -49,7 +53,7 @@ export function ClientProvaUpload() {
setFile(selectedFile); setFile(selectedFile);
// Criar preview para imagens // Criar preview para imagens
if (selectedFile.type.startsWith('image/')) { if (selectedFile.type.startsWith("image/")) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
setFilePreview(reader.result as string); setFilePreview(reader.result as string);
@@ -64,17 +68,17 @@ export function ClientProvaUpload() {
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'], "image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"],
'application/pdf': ['.pdf'] "application/pdf": [".pdf"],
}, },
multiple: false, multiple: false,
maxSize: 10 * 1024 * 1024 // 10MB maxSize: 10 * 1024 * 1024, // 10MB
}); });
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!file) { if (!file) {
toast.error('Por favor, selecione um arquivo primeiro'); toast.error("Por favor, selecione um arquivo primeiro");
return; return;
} }
@@ -85,44 +89,52 @@ export function ClientProvaUpload() {
const formData = new FormData(); const formData = new FormData();
// Adicionar arquivo com todas as informações preservadas // Adicionar arquivo com todas as informações preservadas
formData.append('files', file); formData.append("files", file);
formData.append('modification_times', String(file.lastModified)); formData.append("modification_times", String(file.lastModified));
formData.append('file_names', file.name); formData.append("file_names", file.name);
formData.append('file_sizes', String(file.size)); formData.append("file_sizes", String(file.size));
formData.append('file_types', file.type); formData.append("file_types", file.type);
// Adicionar metadados de contexto // Adicionar metadados de contexto
formData.append('user_agent', navigator.userAgent); formData.append("user_agent", navigator.userAgent);
formData.append('language', navigator.language); formData.append("language", navigator.language);
formData.append('viewport_width', String(window.innerWidth)); formData.append("viewport_width", String(window.innerWidth));
formData.append('viewport_height', String(window.innerHeight)); formData.append("viewport_height", String(window.innerHeight));
formData.append('referrer', document.referrer); formData.append("referrer", document.referrer);
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); formData.append(
formData.append('timezone_offset', String(new Date().getTimezoneOffset())); "timezone",
formData.append('platform', navigator.platform); Intl.DateTimeFormat().resolvedOptions().timeZone,
formData.append('cookies_enabled', String(navigator.cookieEnabled)); );
formData.append(
"timezone_offset",
String(new Date().getTimezoneOffset()),
);
formData.append("platform", navigator.platform);
formData.append("cookies_enabled", String(navigator.cookieEnabled));
// Adicionar informações de browser detalhadas // Adicionar informações de browser detalhadas
try { try {
const pluginsInfo = Array.from(navigator.plugins).map(p => p.name).join(','); const pluginsInfo = Array.from(navigator.plugins)
formData.append('browser_plugins', pluginsInfo); .map((p) => p.name)
.join(",");
formData.append("browser_plugins", pluginsInfo);
} catch (e) { } catch (e) {
formData.append('browser_plugins', 'não disponível'); formData.append("browser_plugins", "não disponível");
} }
// Adicionar dados de localização, se disponíveis // Adicionar dados de localização, se disponíveis
if (userLocation) { if (userLocation) {
formData.append('latitude', String(userLocation.latitude)); formData.append("latitude", String(userLocation.latitude));
formData.append('longitude', String(userLocation.longitude)); formData.append("longitude", String(userLocation.longitude));
} }
formData.append('location_permission', locationPermission); formData.append("location_permission", locationPermission);
// Adicionar timestamp preciso do cliente // Adicionar timestamp preciso do cliente
formData.append('client_timestamp', new Date().toISOString()); formData.append("client_timestamp", new Date().toISOString());
// Criar simulação de progresso // Criar simulação de progresso
const progressInterval = setInterval(() => { const progressInterval = setInterval(() => {
setUploadProgress(prev => { setUploadProgress((prev) => {
if (prev >= 95) { if (prev >= 95) {
clearInterval(progressInterval); clearInterval(progressInterval);
return prev; return prev;
@@ -131,8 +143,8 @@ export function ClientProvaUpload() {
}); });
}, 300); }, 300);
const response = await fetch('/api/provas/upload', { const response = await fetch("/api/provas/upload", {
method: 'POST', method: "POST",
body: formData, body: formData,
}); });
@@ -146,9 +158,10 @@ export function ClientProvaUpload() {
try { try {
const errorJson = JSON.parse(errorText); const errorJson = JSON.parse(errorText);
errorMessage = errorJson.error || errorJson.details || 'Erro ao enviar arquivo'; errorMessage =
errorJson.error || errorJson.details || "Erro ao enviar arquivo";
} catch { } catch {
errorMessage = errorText || 'Erro ao enviar arquivo'; errorMessage = errorText || "Erro ao enviar arquivo";
} }
throw new Error(errorMessage); throw new Error(errorMessage);
@@ -158,17 +171,18 @@ export function ClientProvaUpload() {
const result = await response.json(); const result = await response.json();
if (result.id) { if (result.id) {
toast.success('Arquivo enviado com sucesso!'); toast.success("Arquivo enviado com sucesso!");
// Navegar para a página de status // Navegar para a página de status
navigate(`/public/provas/status/${result.id}`); navigate(`/public/provas/status/${result.id}`);
} else { } else {
throw new Error('ID do documento não encontrado na resposta'); throw new Error("ID do documento não encontrado na resposta");
} }
} catch (error) { } catch (error) {
console.error('Erro detalhado no upload:', error); console.error("Erro detalhado no upload:", error);
setError(error.message || 'Erro ao enviar arquivo. Por favor, tente novamente.'); setError(
toast.error('Falha no upload: ' + error.message); error.message || "Erro ao enviar arquivo. Por favor, tente novamente.",
);
toast.error("Falha no upload: " + error.message);
} finally { } finally {
setUploading(false); setUploading(false);
} }
@@ -178,15 +192,20 @@ export function ClientProvaUpload() {
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 flex flex-col justify-center"> <div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 flex flex-col justify-center">
<div className="max-w-3xl mx-auto w-full"> <div className="max-w-3xl mx-auto w-full">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Validação Digital de Documentos</h1> <h1 className="text-3xl font-bold text-gray-900">
Validação Digital de Documentos
</h1>
<p className="mt-2 text-lg text-gray-600"> <p className="mt-2 text-lg text-gray-600">
Valide e proteja a autenticidade de seus documentos e imagens com timestamping seguro Valide e proteja a autenticidade de seus documentos e imagens com
timestamping seguro
</p> </p>
</div> </div>
<div className="bg-white shadow overflow-hidden rounded-lg"> <div className="bg-white shadow overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:px-6 bg-blue-700 text-white"> <div className="px-4 py-5 sm:px-6 bg-blue-700 text-white">
<h2 className="text-xl font-semibold">Upload de Documento para Validação</h2> <h2 className="text-xl font-semibold">
Upload de Documento para Validação
</h2>
<p className="mt-1 text-sm text-blue-100"> <p className="mt-1 text-sm text-blue-100">
Formatos aceitos: JPG, PNG, GIF, PDF (máx. 10MB) Formatos aceitos: JPG, PNG, GIF, PDF (máx. 10MB)
</p> </p>
@@ -195,7 +214,9 @@ export function ClientProvaUpload() {
<div <div
{...getRootProps()} {...getRootProps()}
className={`border-2 border-dashed rounded-lg p-10 text-center cursor-pointer transition-colors ${ className={`border-2 border-dashed rounded-lg p-10 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-400' isDragActive
? "border-blue-500 bg-blue-50"
: "border-gray-300 hover:border-blue-400"
}`} }`}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
@@ -210,9 +231,11 @@ export function ClientProvaUpload() {
/> />
</div> </div>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{file?.name} ({(file?.size || 0) / 1024 < 1024 {file?.name} (
{(file?.size || 0) / 1024 < 1024
? `${Math.round((file?.size || 0) / 1024)} KB` ? `${Math.round((file?.size || 0) / 1024)} KB`
: `${Math.round((file?.size || 0) / 1024 / 1024 * 10) / 10} MB`}) : `${Math.round(((file?.size || 0) / 1024 / 1024) * 10) / 10} MB`}
)
</p> </p>
<button <button
onClick={(e) => { onClick={(e) => {
@@ -227,8 +250,17 @@ export function ClientProvaUpload() {
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<svg className="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48"> <svg
<path d="M24 8v16m0 0v16m0-16h16m-16 0H8" strokeWidth="2" strokeLinecap="round" /> className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
>
<path
d="M24 8v16m0 0v16m0-16h16m-16 0H8"
strokeWidth="2"
strokeLinecap="round"
/>
</svg> </svg>
<p className="text-lg font-medium text-gray-900"> <p className="text-lg font-medium text-gray-900">
{isDragActive {isDragActive
@@ -236,21 +268,24 @@ export function ClientProvaUpload() {
: "Arraste e solte um arquivo aqui, ou clique para selecionar"} : "Arraste e solte um arquivo aqui, ou clique para selecionar"}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Os metadados originais e informações de contexto serão preservados para melhor validação Os metadados originais e informações de contexto serão
preservados para melhor validação
</p> </p>
</div> </div>
)} )}
</div> </div>
{locationPermission === 'granted' && ( {locationPermission === "granted" && (
<div className="mt-4 text-sm text-green-600"> <div className="mt-4 text-sm text-green-600">
✓ Localização obtida: Esta informação ajudará na validação do documento ✓ Localização obtida: Esta informação ajudará na validação do
documento
</div> </div>
)} )}
{locationPermission === 'denied' && ( {locationPermission === "denied" && (
<div className="mt-4 text-sm text-yellow-600"> <div className="mt-4 text-sm text-yellow-600">
⚠️ Localização não permitida: A validação ainda funcionará, mas sem dados de geolocalização ⚠️ Localização não permitida: A validação ainda funcionará, mas
sem dados de geolocalização
</div> </div>
)} )}
@@ -278,8 +313,8 @@ export function ClientProvaUpload() {
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-blue-200"> <div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-blue-200">
<div <div
style={{ width: `${uploadProgress}%` }} style={{ width: `${uploadProgress}%` }}
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-500 transition-all duration-300"> className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-blue-500 transition-all duration-300"
</div> ></div>
</div> </div>
</div> </div>
</div> </div>
@@ -290,20 +325,22 @@ export function ClientProvaUpload() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={!file || uploading} disabled={!file || uploading}
className={`px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white className={`px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white
${!file || uploading ${
? 'bg-gray-300 cursor-not-allowed' !file || uploading
: 'bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' ? "bg-gray-300 cursor-not-allowed"
: "bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
}`} }`}
> >
{uploading ? 'Enviando...' : 'Enviar para Validação'} {uploading ? "Enviando..." : "Enviar para Validação"}
</button> </button>
</div> </div>
</div> </div>
<div className="px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200"> <div className="px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200">
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
<strong>Nota sobre privacidade:</strong> Este serviço coleta metadados como informações do dispositivo, <strong>Nota sobre privacidade:</strong> Este serviço coleta
localização e timestamp para garantir a validação adequada do documento. Todos os dados são criptografados metadados como informações do dispositivo, localização e timestamp
e armazenados com segurança. para garantir a validação adequada do documento. Todos os dados
são criptografados e armazenados com segurança.
</div> </div>
</div> </div>
</div> </div>
+36 -26
View File
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'; import { format } from "date-fns";
import { useParams, useNavigate } from 'react-router-dom'; import { useEffect, useState } from "react";
import { Card } from '../../components/ui/Card'; import { useParams, useNavigate } from "react-router-dom";
import { format } from 'date-fns';
import { Card } from "../../components/ui/Card";
interface ProvaStatus { interface ProvaStatus {
id: string; id: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO'; status: "PENDENTE" | "PROCESSANDO" | "CONCLUIDO" | "ERRO";
fileName: string; fileName: string;
checksumSha256: string; checksumSha256: string;
checksumSha512: string; checksumSha512: string;
@@ -28,19 +29,19 @@ export function ProvaStatus() {
// Verificar se a resposta foi bem-sucedida // Verificar se a resposta foi bem-sucedida
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(errorText || 'Erro ao buscar status'); throw new Error(errorText || "Erro ao buscar status");
} }
const data = await response.json(); const data = await response.json();
setStatus(data); setStatus(data);
// Continuar verificando se ainda estiver processando // Continuar verificando se ainda estiver processando
if (data.status === 'PENDENTE' || data.status === 'PROCESSANDO') { if (data.status === "PENDENTE" || data.status === "PROCESSANDO") {
setTimeout(checkStatus, 2000); setTimeout(checkStatus, 2000);
} }
} catch (error) { } catch (error) {
console.error('Erro detalhado ao verificar status:', error); console.error("Erro detalhado ao verificar status:", error);
setError('Erro ao verificar status da prova'); setError("Erro ao verificar status da prova");
} }
}; };
@@ -48,8 +49,8 @@ export function ProvaStatus() {
}, [id]); }, [id]);
const handleDownloadReport = () => { const handleDownloadReport = () => {
if (status && status.status === 'CONCLUIDO') { if (status && status.status === "CONCLUIDO") {
window.open(`/api/provas/relatorio/${status.checksumSha256}`, '_blank'); window.open(`/api/provas/relatorio/${status.checksumSha256}`, "_blank");
} }
}; };
@@ -59,13 +60,14 @@ export function ProvaStatus() {
} }
}; };
if (error) return ( if (error)
return (
<div className="max-w-3xl mx-auto p-4"> <div className="max-w-3xl mx-auto p-4">
<Card> <Card>
<div className="p-6 text-center"> <div className="p-6 text-center">
<div className="text-red-600 mb-4">{error}</div> <div className="text-red-600 mb-4">{error}</div>
<button <button
onClick={() => navigate('/provas/upload')} onClick={() => navigate("/provas/upload")}
className="px-4 py-2 bg-blue-600 text-white rounded-lg" className="px-4 py-2 bg-blue-600 text-white rounded-lg"
> >
Voltar para Upload Voltar para Upload
@@ -75,7 +77,8 @@ export function ProvaStatus() {
</div> </div>
); );
if (!status) return ( if (!status)
return (
<div className="max-w-3xl mx-auto p-4"> <div className="max-w-3xl mx-auto p-4">
<Card> <Card>
<div className="p-6 text-center"> <div className="p-6 text-center">
@@ -99,19 +102,26 @@ export function ProvaStatus() {
<div> <div>
<p className="text-sm text-gray-500">Status</p> <p className="text-sm text-gray-500">Status</p>
<p className={`font-medium ${ <p
status.status === 'CONCLUIDO' ? 'text-green-600' : className={`font-medium ${
status.status === 'ERRO' ? 'text-red-600' : status.status === "CONCLUIDO"
'text-blue-600' ? "text-green-600"
}`}> : status.status === "ERRO"
{status.status === 'PENDENTE' ? 'Em fila de processamento' : ? "text-red-600"
status.status === 'PROCESSANDO' ? 'Processando...' : : "text-blue-600"
status.status === 'CONCLUIDO' ? 'Concluído' : }`}
'Erro ao processar'} >
{status.status === "PENDENTE"
? "Em fila de processamento"
: status.status === "PROCESSANDO"
? "Processando..."
: status.status === "CONCLUIDO"
? "Concluído"
: "Erro ao processar"}
</p> </p>
</div> </div>
{status.status === 'CONCLUIDO' && ( {status.status === "CONCLUIDO" && (
<> <>
<div> <div>
<p className="text-sm text-gray-500">Checksums</p> <p className="text-sm text-gray-500">Checksums</p>
@@ -140,14 +150,14 @@ export function ProvaStatus() {
</> </>
)} )}
{status.status === 'ERRO' && ( {status.status === "ERRO" && (
<div className="text-red-600"> <div className="text-red-600">
<p className="text-sm">Erro ao processar arquivo:</p> <p className="text-sm">Erro ao processar arquivo:</p>
<p className="mt-1">{status.error}</p> <p className="mt-1">{status.error}</p>
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => navigate('/provas/upload')} onClick={() => navigate("/provas/upload")}
className="px-4 py-2 bg-blue-600 text-white rounded-lg" className="px-4 py-2 bg-blue-600 text-white rounded-lg"
> >
Tentar Novamente Tentar Novamente
+78 -56
View File
@@ -1,47 +1,53 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from "react";
import { useDropzone } from 'react-dropzone'; import { useDropzone } from "react-dropzone";
import { Card } from '../../components/ui/Card'; import { useNavigate } from "react-router-dom";
import { useNavigate } from 'react-router-dom';
import { Card } from "../../components/ui/Card";
export function ProvaUpload() { export function ProvaUpload() {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [userLocation, setUserLocation] = useState<{
const [locationPermission, setLocationPermission] = useState<string>('pending'); latitude: number;
longitude: number;
} | null>(null);
const [locationPermission, setLocationPermission] =
useState<string>("pending");
const navigate = useNavigate(); const navigate = useNavigate();
// Tentar obter a localização do usuário quando o componente montar // Tentar obter a localização do usuário quando o componente montar
useEffect(() => { useEffect(() => {
if (navigator.geolocation) { if (navigator.geolocation) {
setLocationPermission('requesting'); setLocationPermission("requesting");
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(position) => { (position) => {
console.log('Localização obtida com sucesso'); console.log("Localização obtida com sucesso");
setUserLocation({ setUserLocation({
latitude: position.coords.latitude, latitude: position.coords.latitude,
longitude: position.coords.longitude longitude: position.coords.longitude,
}); });
setLocationPermission('granted'); setLocationPermission("granted");
}, },
(error) => { (error) => {
console.log('Erro ao obter localização:', error.message); console.log("Erro ao obter localização:", error.message);
setLocationPermission('denied'); setLocationPermission("denied");
}, },
{ {
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 5000, timeout: 5000,
maximumAge: 0 maximumAge: 0,
} },
); );
} else { } else {
console.log('Geolocalização não suportada pelo navegador'); console.log("Geolocalização não suportada pelo navegador");
setLocationPermission('unsupported'); setLocationPermission("unsupported");
} }
}, []); }, []);
const onDrop = useCallback(async (acceptedFiles: File[]) => { const onDrop = useCallback(
async (acceptedFiles: File[]) => {
try { try {
setUploading(true); setUploading(true);
setUploadProgress(0); setUploadProgress(0);
@@ -51,45 +57,53 @@ export function ProvaUpload() {
// Adicionar informações de cada arquivo // Adicionar informações de cada arquivo
acceptedFiles.forEach((file) => { acceptedFiles.forEach((file) => {
formData.append('files', file); formData.append("files", file);
formData.append('modification_times', String(file.lastModified)); formData.append("modification_times", String(file.lastModified));
formData.append('file_names', file.name); formData.append("file_names", file.name);
formData.append('file_sizes', String(file.size)); formData.append("file_sizes", String(file.size));
formData.append('file_types', file.type); formData.append("file_types", file.type);
}); });
// Adicionar metadados de contexto // Adicionar metadados de contexto
formData.append('user_agent', navigator.userAgent); formData.append("user_agent", navigator.userAgent);
formData.append('language', navigator.language); formData.append("language", navigator.language);
formData.append('viewport_width', String(window.innerWidth)); formData.append("viewport_width", String(window.innerWidth));
formData.append('viewport_height', String(window.innerHeight)); formData.append("viewport_height", String(window.innerHeight));
formData.append('referrer', document.referrer); formData.append("referrer", document.referrer);
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); formData.append(
formData.append('timezone_offset', String(new Date().getTimezoneOffset())); "timezone",
formData.append('platform', navigator.platform); Intl.DateTimeFormat().resolvedOptions().timeZone,
formData.append('cookies_enabled', String(navigator.cookieEnabled)); );
formData.append(
"timezone_offset",
String(new Date().getTimezoneOffset()),
);
formData.append("platform", navigator.platform);
formData.append("cookies_enabled", String(navigator.cookieEnabled));
// Adicionar informações de plugins do navegador (com segurança) // Adicionar informações de plugins do navegador (com segurança)
try { try {
const pluginsInfo = Array.from(navigator.plugins).map(p => p.name).join(','); const pluginsInfo = Array.from(navigator.plugins)
formData.append('browser_plugins', pluginsInfo); .map((p) => p.name)
.join(",");
formData.append("browser_plugins", pluginsInfo);
} catch (e) { } catch (e) {
formData.append('browser_plugins', 'não disponível'); formData.append("browser_plugins", "não disponível");
} }
// Adicionar dados de localização, se disponíveis // Adicionar dados de localização, se disponíveis
if (userLocation) { if (userLocation) {
formData.append('latitude', String(userLocation.latitude)); formData.append("latitude", String(userLocation.latitude));
formData.append('longitude', String(userLocation.longitude)); formData.append("longitude", String(userLocation.longitude));
} }
formData.append('location_permission', locationPermission); formData.append("location_permission", locationPermission);
// Adicionar timestamp preciso // Adicionar timestamp preciso
formData.append('client_timestamp', new Date().toISOString()); formData.append("client_timestamp", new Date().toISOString());
// Criar simulação de progresso // Criar simulação de progresso
const progressInterval = setInterval(() => { const progressInterval = setInterval(() => {
setUploadProgress(prev => { setUploadProgress((prev) => {
if (prev >= 95) { if (prev >= 95) {
clearInterval(progressInterval); clearInterval(progressInterval);
return prev; return prev;
@@ -98,8 +112,8 @@ export function ProvaUpload() {
}); });
}, 300); }, 300);
const response = await fetch('/api/provas/upload', { const response = await fetch("/api/provas/upload", {
method: 'POST', method: "POST",
body: formData, body: formData,
}); });
@@ -115,10 +129,11 @@ export function ProvaUpload() {
try { try {
// Tentar parsear como JSON // Tentar parsear como JSON
const errorData = JSON.parse(errorText); const errorData = JSON.parse(errorText);
errorMessage = errorData.error || errorData.details || 'Erro ao enviar arquivos'; errorMessage =
errorData.error || errorData.details || "Erro ao enviar arquivos";
} catch { } catch {
// Se não for JSON, usar o texto como está // Se não for JSON, usar o texto como está
errorMessage = errorText || 'Erro ao enviar arquivos'; errorMessage = errorText || "Erro ao enviar arquivos";
} }
throw new Error(errorMessage); throw new Error(errorMessage);
@@ -131,23 +146,27 @@ export function ProvaUpload() {
// Navegar para a página de status // Navegar para a página de status
navigate(`/provas/status/${result.id}`); navigate(`/provas/status/${result.id}`);
} else { } else {
throw new Error('ID do documento não encontrado na resposta'); throw new Error("ID do documento não encontrado na resposta");
} }
} catch (error) { } catch (error) {
console.error('Erro detalhado no upload:', error); console.error("Erro detalhado no upload:", error);
setError(error.message || 'Erro ao enviar arquivos. Por favor, tente novamente.'); setError(
error.message ||
"Erro ao enviar arquivos. Por favor, tente novamente.",
);
} finally { } finally {
setUploading(false); setUploading(false);
} }
}, [navigate, userLocation, locationPermission]); },
[navigate, userLocation, locationPermission],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
'image/*': ['.jpg', '.jpeg', '.png', '.gif'], "image/*": [".jpg", ".jpeg", ".png", ".gif"],
'application/pdf': ['.pdf'] "application/pdf": [".pdf"],
} },
}); });
return ( return (
@@ -163,7 +182,7 @@ export function ProvaUpload() {
{...getRootProps()} {...getRootProps()}
className={` className={`
border-2 border-dashed p-12 rounded-lg border-2 border-dashed p-12 rounded-lg
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'} ${isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300"}
cursor-pointer hover:border-blue-500 transition-colors cursor-pointer hover:border-blue-500 transition-colors
`} `}
> >
@@ -182,8 +201,8 @@ export function ProvaUpload() {
<div> <div>
<p className="text-gray-600"> <p className="text-gray-600">
{isDragActive {isDragActive
? 'Solte os arquivos aqui...' ? "Solte os arquivos aqui..."
: 'Arraste e solte arquivos aqui, ou clique para selecionar'} : "Arraste e solte arquivos aqui, ou clique para selecionar"}
</p> </p>
<p className="text-sm text-gray-500 mt-2"> <p className="text-sm text-gray-500 mt-2">
Arquivos suportados: PDF, JPG, PNG, GIF Arquivos suportados: PDF, JPG, PNG, GIF
@@ -209,9 +228,12 @@ export function ProvaUpload() {
<li>Gerar relatório PDF com todas as informações</li> <li>Gerar relatório PDF com todas as informações</li>
</ul> </ul>
{locationPermission === 'denied' && ( {locationPermission === "denied" && (
<div className="mt-4 p-2 bg-yellow-50 text-yellow-700 rounded"> <div className="mt-4 p-2 bg-yellow-50 text-yellow-700 rounded">
<p>Permissão de localização negada. A validação prosseguirá sem dados de geolocalização.</p> <p>
Permissão de localização negada. A validação prosseguirá sem
dados de geolocalização.
</p>
</div> </div>
)} )}
</div> </div>
+156 -66
View File
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { format } from "date-fns";
import { useParams, Link, useNavigate } from 'react-router-dom'; import { ptBR } from "date-fns/locale";
import { format } from 'date-fns'; import { useEffect, useState } from "react";
import ptBR from 'date-fns/locale/pt-BR'; import { useParams, Link, useNavigate } from "react-router-dom";
interface ProvaStatus { interface ProvaStatus {
id: string; id: string;
status: 'PENDENTE' | 'PROCESSANDO' | 'CONCLUIDO' | 'ERRO'; status: "PENDENTE" | "PROCESSANDO" | "CONCLUIDO" | "ERRO";
fileName: string; fileName: string;
checksumSha256: string; checksumSha256: string;
checksumSha512: string; checksumSha512: string;
@@ -39,20 +39,20 @@ export function PublicProvaStatus() {
// Verificar se a resposta foi bem-sucedida // Verificar se a resposta foi bem-sucedida
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(errorText || 'Erro ao buscar status'); throw new Error(errorText || "Erro ao buscar status");
} }
const data = await response.json(); const data = await response.json();
setStatus(data); setStatus(data);
setCheckCount(prev => prev + 1); setCheckCount((prev) => prev + 1);
// Continuar verificando se ainda estiver processando // Continuar verificando se ainda estiver processando
if (data.status === 'PENDENTE' || data.status === 'PROCESSANDO') { if (data.status === "PENDENTE" || data.status === "PROCESSANDO") {
setTimeout(checkStatus, 2000); setTimeout(checkStatus, 2000);
} }
} catch (error) { } catch (error) {
console.error('Erro detalhado ao verificar status:', error); console.error("Erro detalhado ao verificar status:", error);
setError('Erro ao verificar status da prova'); setError("Erro ao verificar status da prova");
} }
}; };
@@ -60,8 +60,8 @@ export function PublicProvaStatus() {
}, [id]); }, [id]);
const handleDownloadReport = () => { const handleDownloadReport = () => {
if (status && status.status === 'CONCLUIDO') { if (status && status.status === "CONCLUIDO") {
window.open(`/api/provas/relatorio/${status.checksumSha256}`, '_blank'); window.open(`/api/provas/relatorio/${status.checksumSha256}`, "_blank");
} }
}; };
@@ -71,15 +71,29 @@ export function PublicProvaStatus() {
} }
}; };
if (error) return ( if (error)
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<div className="bg-white shadow-md rounded-lg"> <div className="bg-white shadow-md rounded-lg">
<div className="p-6 text-center"> <div className="p-6 text-center">
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-16 h-16 text-red-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<div className="text-red-600 text-xl font-bold mb-4">Ocorreu um erro</div> <div className="text-red-600 text-xl font-bold mb-4">
Ocorreu um erro
</div>
<p className="text-gray-700 mb-6">{error}</p> <p className="text-gray-700 mb-6">{error}</p>
<Link <Link
to="/public/provas/upload" to="/public/provas/upload"
@@ -93,14 +107,17 @@ export function PublicProvaStatus() {
</div> </div>
); );
if (!status && checkCount < 3) return ( if (!status && checkCount < 3)
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<div className="bg-white shadow-md rounded-lg"> <div className="bg-white shadow-md rounded-lg">
<div className="p-6 text-center"> <div className="p-6 text-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div> <div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-700 text-lg">Carregando informações...</p> <p className="text-gray-700 text-lg">Carregando informações...</p>
<p className="text-gray-500 text-sm mt-2">Verificando status do seu documento</p> <p className="text-gray-500 text-sm mt-2">
Verificando status do seu documento
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -117,26 +134,60 @@ export function PublicProvaStatus() {
<div className="p-6"> <div className="p-6">
<div className="mb-8"> <div className="mb-8">
<div className={`text-center p-6 rounded-lg ${ <div
status?.status === 'CONCLUIDO' ? 'bg-green-50' : className={`text-center p-6 rounded-lg ${
status?.status === 'ERRO' ? 'bg-red-50' : status?.status === "CONCLUIDO"
'bg-blue-50' ? "bg-green-50"
}`}> : status?.status === "ERRO"
{status?.status === 'CONCLUIDO' ? ( ? "bg-red-50"
: "bg-blue-50"
}`}
>
{status?.status === "CONCLUIDO" ? (
<> <>
<svg className="w-16 h-16 text-green-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-16 h-16 text-green-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<h3 className="text-2xl font-bold text-green-700 mb-2">Validação Concluída</h3> <h3 className="text-2xl font-bold text-green-700 mb-2">
<p className="text-gray-700">Seu documento foi validado com sucesso</p> Validação Concluída
</h3>
<p className="text-gray-700">
Seu documento foi validado com sucesso
</p>
</> </>
) : status?.status === 'ERRO' ? ( ) : status?.status === "ERRO" ? (
<> <>
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-16 h-16 text-red-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<h3 className="text-2xl font-bold text-red-700 mb-2">Falha na Validação</h3> <h3 className="text-2xl font-bold text-red-700 mb-2">
<p className="text-gray-700">Ocorreu um erro durante o processamento</p> Falha na Validação
</h3>
<p className="text-gray-700">
Ocorreu um erro durante o processamento
</p>
</> </>
) : ( ) : (
<> <>
@@ -144,108 +195,146 @@ export function PublicProvaStatus() {
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div> <div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500"></div>
</div> </div>
<h3 className="text-2xl font-bold text-blue-700 mb-2"> <h3 className="text-2xl font-bold text-blue-700 mb-2">
{status?.status === 'PENDENTE' ? 'Aguardando Processamento' : 'Processando...'} {status?.status === "PENDENTE"
? "Aguardando Processamento"
: "Processando..."}
</h3> </h3>
<p className="text-gray-700">Seu documento está sendo validado</p> <p className="text-gray-700">
Seu documento está sendo validado
</p>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="mb-6"> <div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-gray-800">Detalhes do Documento</h3> <h3 className="text-lg font-semibold mb-3 text-gray-800">
Detalhes do Documento
</h3>
<div className="bg-gray-50 rounded-lg p-4 space-y-3"> <div className="bg-gray-50 rounded-lg p-4 space-y-3">
<div> <div>
<p className="text-sm text-gray-500">Arquivo</p> <p className="text-sm text-gray-500">Arquivo</p>
<p className="font-medium text-gray-800">{status?.fileName}</p> <p className="font-medium text-gray-800">
{status?.fileName}
</p>
</div> </div>
{status?.status === 'CONCLUIDO' && ( {status?.status === "CONCLUIDO" && (
<> <>
<div> <div>
<p className="text-sm text-gray-500">Identificador Digital (SHA-256)</p> <p className="text-sm text-gray-500">
<p className="font-mono text-xs text-gray-700 break-all">{status.checksumSha256}</p> Identificador Digital (SHA-256)
</p>
<p className="font-mono text-xs text-gray-700 break-all">
{status.checksumSha256}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Data de Processamento</p> <p className="text-sm text-gray-500">
Data de Processamento
</p>
<p className="text-gray-700"> <p className="text-gray-700">
{format(new Date(), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })} {format(new Date(), "dd/MM/yyyy HH:mm:ss", {
locale: ptBR,
})}
</p> </p>
</div> </div>
{/* Garantir exibição da data de modificação */} {/* Garantir exibição da data de modificação */}
{(status.metadata?.file_specific?.modified || status.metadata?.modification_times) && ( {(status.metadata?.file_specific?.modified ||
status.metadata?.modification_times) && (
<div> <div>
<p className="text-sm text-gray-500">Data de Modificação Original</p> <p className="text-sm text-gray-500">
Data de Modificação Original
</p>
<p className="text-gray-700"> <p className="text-gray-700">
{format( {format(
new Date( new Date(
status.metadata.file_specific?.modified || status.metadata.file_specific?.modified ||
(status.metadata.modification_times ? (status.metadata.modification_times
parseInt(status.metadata.modification_times) : Date.now()) ? parseInt(status.metadata.modification_times)
: Date.now()),
), ),
'dd/MM/yyyy HH:mm:ss', "dd/MM/yyyy HH:mm:ss",
{ locale: ptBR } { locale: ptBR },
)} )}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Esta é a data real da última modificação do arquivo, obtida dos metadados originais Esta é a data real da última modificação do arquivo,
obtida dos metadados originais
</p> </p>
</div> </div>
)} )}
{/* Informações de conexão aprimoradas */} {/* Informações de conexão aprimoradas */}
<div> <div>
<p className="text-sm text-gray-500">Informações de Conexão</p> <p className="text-sm text-gray-500">
Informações de Conexão
</p>
<p className="text-gray-700"> <p className="text-gray-700">
IP: {status.metadata?.ipAddress || IP:{" "}
{status.metadata?.ipAddress ||
status.metadata?.connectionInfo?.ip || status.metadata?.connectionInfo?.ip ||
status.metadata?.connectionInfo?.socketAddress || '::1'}, status.metadata?.connectionInfo?.socketAddress ||
Porta: {status.metadata?.clientPort || "::1"}
status.metadata?.connectionInfo?.socketPort || '61465'} , Porta:{" "}
{status.metadata?.clientPort ||
status.metadata?.connectionInfo?.socketPort ||
"61465"}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Família: {status.metadata?.connectionInfo?.remoteFamily || Família:{" "}
((status.metadata?.ipAddress || '::1').includes(':') ? 'IPv6' : 'IPv4')} {status.metadata?.connectionInfo?.remoteFamily ||
((status.metadata?.ipAddress || "::1").includes(":")
? "IPv6"
: "IPv4")}
</p> </p>
</div> </div>
{/* Adicionar geolocalização se disponível */} {/* Adicionar geolocalização se disponível */}
{(status.metadata?.latitude !== undefined || status.metadata?.longitude !== undefined) && ( {(status.metadata?.latitude !== undefined ||
status.metadata?.longitude !== undefined) && (
<div> <div>
<p className="text-sm text-gray-500">Geolocalização</p> <p className="text-sm text-gray-500">Geolocalização</p>
<p className="text-gray-700"> <p className="text-gray-700">
Coordenadas: {status.metadata.latitude || 'N/A'}, {status.metadata.longitude || 'N/A'} Coordenadas: {status.metadata.latitude || "N/A"},{" "}
{status.metadata.longitude || "N/A"}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Permissão: {status.metadata.location_permission || 'não especificada'} Permissão:{" "}
{status.metadata.location_permission ||
"não especificada"}
</p> </p>
</div> </div>
)} )}
</> </>
)} )}
{status?.status === 'PROCESSANDO' && ( {status?.status === "PROCESSANDO" && (
<div className="pt-2"> <div className="pt-2">
<div className="w-full bg-gray-200 rounded-full h-2.5"> <div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-blue-600 h-2.5 rounded-full w-3/4"></div> <div className="bg-blue-600 h-2.5 rounded-full w-3/4"></div>
</div> </div>
<p className="text-sm text-gray-500 mt-2">Validando autenticidade e gerando relatório...</p> <p className="text-sm text-gray-500 mt-2">
Validando autenticidade e gerando relatório...
</p>
</div> </div>
)} )}
{status?.status === 'ERRO' && ( {status?.status === "ERRO" && (
<div className="bg-red-50 border border-red-200 rounded-md p-3 text-red-700"> <div className="bg-red-50 border border-red-200 rounded-md p-3 text-red-700">
<p className="font-medium">Erro no processamento:</p> <p className="font-medium">Erro no processamento:</p>
<p className="text-sm">{status.error || 'Falha não especificada'}</p> <p className="text-sm">
{status.error || "Falha não especificada"}
</p>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
{status?.status === 'CONCLUIDO' && ( {status?.status === "CONCLUIDO" && (
<> <>
<button <button
onClick={handleViewVerification} onClick={handleViewVerification}
@@ -263,7 +352,7 @@ export function PublicProvaStatus() {
</> </>
)} )}
{status?.status === 'ERRO' && ( {status?.status === "ERRO" && (
<Link <Link
to="/public/provas/upload" to="/public/provas/upload"
className="inline-flex items-center justify-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" className="inline-flex items-center justify-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"
@@ -275,7 +364,8 @@ export function PublicProvaStatus() {
</div> </div>
<div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500 rounded-b-lg"> <div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500 rounded-b-lg">
Validação Digital AdvX - Preservando a integridade e autenticidade dos seus documentos Validação Digital AdvX - Preservando a integridade e autenticidade
dos seus documentos
</div> </div>
</div> </div>
</div> </div>
+164 -65
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { format, isValid } from "date-fns";
import { useParams, Link } from 'react-router-dom'; import { ptBR } from "date-fns/locale";
import { format, isValid } from 'date-fns'; import { useEffect, useState } from "react";
import ptBR from 'date-fns/locale/pt-BR'; import { useParams, Link } from "react-router-dom";
interface ProvaVerificacao { interface ProvaVerificacao {
verified: boolean; verified: boolean;
@@ -32,15 +32,15 @@ export function PublicVerificarProva() {
const response = await fetch(`/api/provas/verificar/${hash}`); const response = await fetch(`/api/provas/verificar/${hash}`);
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText || 'Falha na verificação'); throw new Error(response.statusText || "Falha na verificação");
} }
const data = await response.json(); const data = await response.json();
console.log('Dados recebidos:', data); console.log("Dados recebidos:", data);
setResult(data); setResult(data);
} catch (error) { } catch (error) {
console.error('Erro ao verificar:', error); console.error("Erro ao verificar:", error);
setError('Não foi possível verificar a autenticidade do documento.'); setError("Não foi possível verificar a autenticidade do documento.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -53,20 +53,20 @@ export function PublicVerificarProva() {
// Função segura para formatar datas // Função segura para formatar datas
const formatDate = (dateString: string | undefined | null) => { const formatDate = (dateString: string | undefined | null) => {
if (!dateString) return 'Data não disponível'; if (!dateString) return "Data não disponível";
try { try {
const date = new Date(dateString); const date = new Date(dateString);
if (!isValid(date)) { if (!isValid(date)) {
console.warn('Data inválida:', dateString); console.warn("Data inválida:", dateString);
return 'Data inválida'; return "Data inválida";
} }
return format(date, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }); return format(date, "dd/MM/yyyy HH:mm:ss", { locale: ptBR });
} catch (error) { } catch (error) {
console.error('Erro ao formatar data:', error); console.error("Erro ao formatar data:", error);
return 'Erro na formatação da data'; return "Erro na formatação da data";
} }
}; };
@@ -79,7 +79,9 @@ export function PublicVerificarProva() {
<div className="h-4 bg-gray-300 rounded w-3/4 mx-auto mb-4"></div> <div className="h-4 bg-gray-300 rounded w-3/4 mx-auto mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mx-auto"></div> <div className="h-4 bg-gray-300 rounded w-1/2 mx-auto"></div>
</div> </div>
<p className="mt-4 text-gray-500 text-center">Carregando informações...</p> <p className="mt-4 text-gray-500 text-center">
Carregando informações...
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -92,11 +94,26 @@ export function PublicVerificarProva() {
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
<div className="p-6 text-center"> <div className="p-6 text-center">
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-16 h-16 text-red-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<h2 className="text-2xl font-bold text-red-600 mb-4">Verificação Falhou</h2> <h2 className="text-2xl font-bold text-red-600 mb-4">
<p className="text-gray-600 mb-6">{error || 'Não foi possível verificar este documento.'}</p> Verificação Falhou
</h2>
<p className="text-gray-600 mb-6">
{error || "Não foi possível verificar este documento."}
</p>
<Link <Link
to="/provas/upload" to="/provas/upload"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700" className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700"
@@ -118,20 +135,47 @@ export function PublicVerificarProva() {
<div className="text-center mb-8"> <div className="text-center mb-8">
{result.verified ? ( {result.verified ? (
<> <>
<svg className="w-20 h-20 text-green-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-20 h-20 text-green-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<h2 className="text-3xl font-bold text-green-600 mb-2">Documento Verificado</h2> <h2 className="text-3xl font-bold text-green-600 mb-2">
Documento Verificado
</h2>
<p className="text-gray-600 text-lg"> <p className="text-gray-600 text-lg">
Este documento foi validado digitalmente e possui registro de autenticidade. Este documento foi validado digitalmente e possui registro
de autenticidade.
</p> </p>
</> </>
) : ( ) : (
<> <>
<svg className="w-20 h-20 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> className="w-20 h-20 text-red-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
<h2 className="text-3xl font-bold text-red-600 mb-2">Verificação com Problemas</h2> <h2 className="text-3xl font-bold text-red-600 mb-2">
Verificação com Problemas
</h2>
<p className="text-gray-600 text-lg">{result.message}</p> <p className="text-gray-600 text-lg">{result.message}</p>
</> </>
)} )}
@@ -140,45 +184,73 @@ export function PublicVerificarProva() {
{result.prova && ( {result.prova && (
<div className="space-y-6"> <div className="space-y-6">
<div className="border-t border-b border-gray-200 py-6"> <div className="border-t border-b border-gray-200 py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações do Documento</h3> <h3 className="text-xl font-semibold text-gray-800 mb-4">
Informações do Documento
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DetailItem label="Nome do Arquivo" value={result.prova.fileName} /> <DetailItem
<DetailItem label="Data de Validação" value={formatDate(result.prova.dataCriacao)} /> label="Nome do Arquivo"
<DetailItem label="Última Verificação" value={formatDate(result.prova.dataValida)} /> value={result.prova.fileName}
/>
<DetailItem
label="Data de Validação"
value={formatDate(result.prova.dataCriacao)}
/>
<DetailItem
label="Última Verificação"
value={formatDate(result.prova.dataValida)}
/>
</div> </div>
</div> </div>
<div className="border-b border-gray-200 py-6"> <div className="border-b border-gray-200 py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações de Segurança</h3> <h3 className="text-xl font-semibold text-gray-800 mb-4">
Informações de Segurança
</h3>
<div className="space-y-3 font-mono text-sm overflow-auto bg-gray-50 p-4 rounded-lg"> <div className="space-y-3 font-mono text-sm overflow-auto bg-gray-50 p-4 rounded-lg">
<DetailItem label="SHA-256" value={result.prova.checksumSha256} /> <DetailItem
<DetailItem label="SHA-512" value={result.prova.checksumSha512} /> label="SHA-256"
value={result.prova.checksumSha256}
/>
<DetailItem
label="SHA-512"
value={result.prova.checksumSha512}
/>
<DetailItem label="MD5" value={result.prova.checksumMd5} /> <DetailItem label="MD5" value={result.prova.checksumMd5} />
<DetailItem <DetailItem
label="Registro Temporal" label="Registro Temporal"
value={formatDate(result.prova.timestamp ? value={formatDate(
JSON.parse(result.prova.timestamp).timestamp : result.prova.timestamp
result.prova.dataCriacao)} ? JSON.parse(result.prova.timestamp).timestamp
: result.prova.dataCriacao,
)}
/> />
<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>
<dt className="text-sm font-medium text-gray-500">IP:</dt> <dt className="text-sm font-medium text-gray-500">
IP:
</dt>
<dd className="mt-1 text-base text-gray-900 break-words"> <dd className="mt-1 text-base text-gray-900 break-words">
{result.verificacao?.ip || {result.verificacao?.ip ||
result.prova.metadata?.connectionInfo?.forwardedIp || result.prova.metadata?.connectionInfo
(result.prova.metadata?.headers?.['x-forwarded-for'] ? ?.forwardedIp ||
result.prova.metadata.headers['x-forwarded-for'].split(',')[0].trim() : (result.prova.metadata?.headers?.["x-forwarded-for"]
result.prova.metadata?.ipAddress) || ? result.prova.metadata.headers["x-forwarded-for"]
'Não disponível'} .split(",")[0]
.trim()
: result.prova.metadata?.ipAddress) ||
"Não disponível"}
</dd> </dd>
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-gray-500">Porta:</dt> <dt className="text-sm font-medium text-gray-500">
Porta:
</dt>
<dd className="mt-1 text-base text-gray-900 break-words"> <dd className="mt-1 text-base text-gray-900 break-words">
{result.verificacao?.porta || {result.verificacao?.porta ||
result.prova.metadata?.clientPort || result.prova.metadata?.clientPort ||
result.prova.metadata?.connectionInfo?.socketPort || result.prova.metadata?.connectionInfo?.socketPort ||
'Não disponível'} "Não disponível"}
</dd> </dd>
</div> </div>
</div> </div>
@@ -187,21 +259,26 @@ export function PublicVerificarProva() {
{result.prova.metadata && ( {result.prova.metadata && (
<div className="py-6"> <div className="py-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">Informações de Contexto</h3> <h3 className="text-xl font-semibold text-gray-800 mb-4">
Informações de Contexto
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
{result.prova.metadata.userInfo ? ( {result.prova.metadata.userInfo ? (
<> <>
<DetailItem <DetailItem
label="Navegador" label="Navegador"
value={`${result.prova.metadata.userInfo.browser?.name || 'N/A'} ${result.prova.metadata.userInfo.browser?.version || ''}`} value={`${result.prova.metadata.userInfo.browser?.name || "N/A"} ${result.prova.metadata.userInfo.browser?.version || ""}`}
/> />
<DetailItem <DetailItem
label="Sistema Operacional" label="Sistema Operacional"
value={`${result.prova.metadata.userInfo.os?.name || 'N/A'} ${result.prova.metadata.userInfo.os?.version || ''}`} value={`${result.prova.metadata.userInfo.os?.name || "N/A"} ${result.prova.metadata.userInfo.os?.version || ""}`}
/> />
<DetailItem <DetailItem
label="Dispositivo" label="Dispositivo"
value={result.prova.metadata.userInfo.device?.type || 'desktop'} value={
result.prova.metadata.userInfo.device?.type ||
"desktop"
}
/> />
</> </>
) : ( ) : (
@@ -209,13 +286,18 @@ export function PublicVerificarProva() {
{/* Exibir informações do navegador - baseado no user-agent */} {/* Exibir informações do navegador - baseado no user-agent */}
<DetailItem <DetailItem
label="User-Agent" label="User-Agent"
value={result.prova.metadata.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} value={
result.prova.metadata.userAgent ||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
/> />
{/* Exibir informações do sistema */} {/* Exibir informações do sistema */}
<DetailItem <DetailItem
label="Sistema" label="Sistema"
value={result.prova.metadata.platform || 'Windows 10'} value={
result.prova.metadata.platform || "Windows 10"
}
/> />
</> </>
)} )}
@@ -227,7 +309,7 @@ export function PublicVerificarProva() {
result.prova.metadata.ipAddress || result.prova.metadata.ipAddress ||
result.prova.metadata.connectionInfo?.ip || result.prova.metadata.connectionInfo?.ip ||
result.prova.metadata.connectionInfo?.socketAddress || result.prova.metadata.connectionInfo?.socketAddress ||
'::1' "::1"
} }
/> />
@@ -236,8 +318,9 @@ export function PublicVerificarProva() {
label="Porta de Conexão" label="Porta de Conexão"
value={ value={
result.prova.metadata.clientPort || result.prova.metadata.clientPort ||
(result.prova.metadata.connectionInfo?.socketPort ? (result.prova.metadata.connectionInfo?.socketPort
result.prova.metadata.connectionInfo.socketPort : '61465') ? result.prova.metadata.connectionInfo.socketPort
: "61465")
} }
/> />
@@ -246,7 +329,11 @@ export function PublicVerificarProva() {
label="Família IP" label="Família IP"
value={ value={
result.prova.metadata.connectionInfo?.remoteFamily || result.prova.metadata.connectionInfo?.remoteFamily ||
((result.prova.metadata.ipAddress || '::1').includes(':') ? 'IPv6' : 'IPv4') ((result.prova.metadata.ipAddress || "::1").includes(
":",
)
? "IPv6"
: "IPv4")
} }
/> />
@@ -255,8 +342,8 @@ export function PublicVerificarProva() {
label="Idioma" label="Idioma"
value={ value={
result.prova.metadata.language || result.prova.metadata.language ||
result.prova.metadata.headers?.['accept-language'] || result.prova.metadata.headers?.["accept-language"] ||
'pt-BR' "pt-BR"
} }
/> />
@@ -269,27 +356,38 @@ export function PublicVerificarProva() {
)} )}
{/* Garantir exibição de data de modificação */} {/* Garantir exibição de data de modificação */}
{(result.prova.metadata.file_specific?.modified || result.prova.metadata.modification_times) && ( {(result.prova.metadata.file_specific?.modified ||
result.prova.metadata.modification_times) && (
<DetailItem <DetailItem
label="Data de Modificação Original" label="Data de Modificação Original"
value={formatDate(result.prova.metadata.file_specific?.modified || value={formatDate(
(result.prova.metadata.modification_times ? result.prova.metadata.file_specific?.modified ||
new Date(parseInt(result.prova.metadata.modification_times)) : null))} (result.prova.metadata.modification_times
? new Date(
parseInt(
result.prova.metadata.modification_times,
),
)
: null),
)}
/> />
)} )}
{/* Exibir geolocalização se disponível */} {/* Exibir geolocalização se disponível */}
{(result.prova.metadata.latitude !== undefined || result.prova.metadata.longitude !== undefined) && ( {(result.prova.metadata.latitude !== undefined ||
result.prova.metadata.longitude !== undefined) && (
<DetailItem <DetailItem
label="Coordenadas" label="Coordenadas"
value={`${result.prova.metadata.latitude || 'N/A'}, ${result.prova.metadata.longitude || 'N/A'}`} value={`${result.prova.metadata.latitude || "N/A"}, ${result.prova.metadata.longitude || "N/A"}`}
/> />
)} )}
<div className="col-span-1 md:col-span-2 mt-1"> <div className="col-span-1 md:col-span-2 mt-1">
<div className="text-xs text-gray-500 italic"> <div className="text-xs text-gray-500 italic">
A data de modificação representa o momento real em que o arquivo foi alterado pela última vez, A data de modificação representa o momento real em que
sendo muito mais confiável que a data de criação para efeitos de validação temporal. o arquivo foi alterado pela última vez, sendo muito
mais confiável que a data de criação para efeitos de
validação temporal.
</div> </div>
</div> </div>
</div> </div>
@@ -319,7 +417,8 @@ export function PublicVerificarProva() {
</div> </div>
<div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500"> <div className="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500">
Documento validado através do sistema AdvX Provas - Integridade e Autenticidade Digital Documento validado através do sistema AdvX Provas - Integridade e
Autenticidade Digital
</div> </div>
</div> </div>
</div> </div>
+44 -32
View File
@@ -1,8 +1,3 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios'; // Importar axios para fazer a requisição
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { import {
Container, Container,
Box, Box,
@@ -12,8 +7,13 @@ import {
Paper, Paper,
Grid, Grid,
Divider, Divider,
Button Button,
} from '@mui/material'; } from "@mui/material";
import axios from "axios"; // Importar axios para fazer a requisição
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
// Componente para verificação de provas digitais // Componente para verificação de provas digitais
export const VerificarProva: React.FC = () => { export const VerificarProva: React.FC = () => {
@@ -23,7 +23,7 @@ export const VerificarProva: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// URL base da API - usar a mesma do ambiente atual // URL base da API - usar a mesma do ambiente atual
const API_BASE_URL = process.env.REACT_APP_API_URL || '/api'; const API_BASE_URL = process.env.REACT_APP_API_URL || "/api";
useEffect(() => { useEffect(() => {
const fetchProvaDetails = async () => { const fetchProvaDetails = async () => {
@@ -37,32 +37,37 @@ export const VerificarProva: React.FC = () => {
platform: navigator.platform, platform: navigator.platform,
screenWidth: window.screen.width, screenWidth: window.screen.width,
screenHeight: window.screen.height, screenHeight: window.screen.height,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}; };
// Fazer a requisição para verificar a prova // Fazer a requisição para verificar a prova
const response = await axios.get(`${API_BASE_URL}/provas/verificar/${hash}`, { const response = await axios.get(
`${API_BASE_URL}/provas/verificar/${hash}`,
{
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); },
);
// Também registrar a verificação separadamente // Também registrar a verificação separadamente
try { try {
await axios.post(`${API_BASE_URL}/provas/registrar-verificacao`, { await axios.post(`${API_BASE_URL}/provas/registrar-verificacao`, {
hash, hash,
verificationContext: connectionInfo verificationContext: connectionInfo,
}); });
} catch (logError) { } catch (logError) {
console.warn('Erro ao registrar verificação:', logError); console.warn("Erro ao registrar verificação:", logError);
// Não interromper o fluxo principal por erro no log // Não interromper o fluxo principal por erro no log
} }
setProva(response.data); setProva(response.data);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error('Erro ao verificar prova:', error); console.error("Erro ao verificar prova:", error);
setError('Não foi possível verificar este documento. O hash pode ser inválido ou o documento não existe.'); setError(
"Não foi possível verificar este documento. O hash pode ser inválido ou o documento não existe.",
);
setLoading(false); setLoading(false);
} }
}; };
@@ -70,7 +75,7 @@ export const VerificarProva: React.FC = () => {
if (hash) { if (hash) {
fetchProvaDetails(); fetchProvaDetails();
} else { } else {
setError('Hash de verificação não fornecido'); setError("Hash de verificação não fornecido");
setLoading(false); setLoading(false);
} }
}, [hash, API_BASE_URL]); }, [hash, API_BASE_URL]);
@@ -107,7 +112,7 @@ export const VerificarProva: React.FC = () => {
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => window.location.href = '/'} onClick={() => (window.location.href = "/")}
> >
Voltar à página inicial Voltar à página inicial
</Button> </Button>
@@ -121,9 +126,9 @@ export const VerificarProva: React.FC = () => {
const formatarData = (dataString: string) => { const formatarData = (dataString: string) => {
try { try {
const data = new Date(dataString); const data = new Date(dataString);
return format(data, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }); return format(data, "dd/MM/yyyy HH:mm:ss", { locale: ptBR });
} catch (error) { } catch (error) {
return 'Data inválida'; return "Data inválida";
} }
}; };
@@ -132,7 +137,9 @@ export const VerificarProva: React.FC = () => {
<Box mt={4} mb={4}> <Box mt={4} mb={4}>
{prova?.verified ? ( {prova?.verified ? (
<Alert severity="success" variant="filled"> <Alert severity="success" variant="filled">
<Typography variant="h6">Documento Verificado com Sucesso</Typography> <Typography variant="h6">
Documento Verificado com Sucesso
</Typography>
<Typography> <Typography>
Este documento foi validado e sua integridade foi confirmada. Este documento foi validado e sua integridade foi confirmada.
</Typography> </Typography>
@@ -141,7 +148,8 @@ export const VerificarProva: React.FC = () => {
<Alert severity="warning" variant="filled"> <Alert severity="warning" variant="filled">
<Typography variant="h6">Verificação Inconclusiva</Typography> <Typography variant="h6">Verificação Inconclusiva</Typography>
<Typography> <Typography>
{prova?.message || 'Não foi possível confirmar a integridade deste documento.'} {prova?.message ||
"Não foi possível confirmar a integridade deste documento."}
</Typography> </Typography>
</Alert> </Alert>
)} )}
@@ -161,7 +169,7 @@ export const VerificarProva: React.FC = () => {
Nome do arquivo: Nome do arquivo:
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
{prova.prova?.fileName || 'Não disponível'} {prova.prova?.fileName || "Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
@@ -170,7 +178,7 @@ export const VerificarProva: React.FC = () => {
Tipo: Tipo:
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
{prova.prova?.mimeType || 'Não disponível'} {prova.prova?.mimeType || "Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
@@ -180,9 +188,9 @@ export const VerificarProva: React.FC = () => {
</Typography> </Typography>
<Typography <Typography
gutterBottom gutterBottom
sx={{ wordBreak: 'break-all', fontSize: '0.9rem' }} sx={{ wordBreak: "break-all", fontSize: "0.9rem" }}
> >
{prova.prova?.checksumSha256 || 'Não disponível'} {prova.prova?.checksumSha256 || "Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
@@ -191,9 +199,9 @@ export const VerificarProva: React.FC = () => {
Data de registro: Data de registro:
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
{prova.timestamp?.timestamp ? {prova.timestamp?.timestamp
formatarData(prova.timestamp.timestamp) : ? formatarData(prova.timestamp.timestamp)
'Não disponível'} : "Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
@@ -211,7 +219,9 @@ export const VerificarProva: React.FC = () => {
IP: IP:
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
{prova.verificacao?.ip || prova.prova?.metadata?.ipAddress || 'Não disponível'} {prova.verificacao?.ip ||
prova.prova?.metadata?.ipAddress ||
"Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
@@ -220,7 +230,9 @@ export const VerificarProva: React.FC = () => {
Porta: Porta:
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
{prova.verificacao?.porta || prova.prova?.metadata?.clientPort || 'Não disponível'} {prova.verificacao?.porta ||
prova.prova?.metadata?.clientPort ||
"Não disponível"}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
+27 -19
View File
@@ -1,11 +1,12 @@
import React, { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { Anexo } from '../../types/entities';
import { import {
DocumentIcon, DocumentIcon,
XMarkIcon, XMarkIcon,
ArrowDownTrayIcon ArrowDownTrayIcon,
} from '@heroicons/react/24/outline'; } from "@heroicons/react/24/outline";
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Anexo } from "../../types/entities";
interface AnexosListProps { interface AnexosListProps {
anexos: Anexo[]; anexos: Anexo[];
@@ -13,27 +14,30 @@ interface AnexosListProps {
} }
export const AnexosList: React.FC<AnexosListProps> = ({ anexos, onChange }) => { export const AnexosList: React.FC<AnexosListProps> = ({ anexos, onChange }) => {
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback(
const novosAnexos: Anexo[] = acceptedFiles.map(file => ({ (acceptedFiles: File[]) => {
const novosAnexos: Anexo[] = acceptedFiles.map((file) => ({
id: Math.random().toString(), id: Math.random().toString(),
nome: file.name, nome: file.name,
tipo: file.type, tipo: file.type,
tamanho: file.size, tamanho: file.size,
url: URL.createObjectURL(file), url: URL.createObjectURL(file),
tarefaId: '' tarefaId: "",
})); }));
onChange([...anexos, ...novosAnexos]); onChange([...anexos, ...novosAnexos]);
}, [anexos, onChange]); },
[anexos, onChange],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB']; const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}; };
return ( return (
@@ -41,15 +45,17 @@ export const AnexosList: React.FC<AnexosListProps> = ({ anexos, onChange }) => {
<div <div
{...getRootProps()} {...getRootProps()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer
${isDragActive ${
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' isDragActive
: 'border-gray-300 dark:border-gray-600'}`} ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
: "border-gray-300 dark:border-gray-600"
}`}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
<p className="text-gray-600 dark:text-gray-400"> <p className="text-gray-600 dark:text-gray-400">
{isDragActive {isDragActive
? 'Solte os arquivos aqui...' ? "Solte os arquivos aqui..."
: 'Arraste e solte arquivos aqui, ou clique para selecionar'} : "Arraste e solte arquivos aqui, ou clique para selecionar"}
</p> </p>
</div> </div>
@@ -73,14 +79,16 @@ export const AnexosList: React.FC<AnexosListProps> = ({ anexos, onChange }) => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
type="button" type="button"
onClick={() => window.open(anexo.url, '_blank')} onClick={() => window.open(anexo.url, "_blank")}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300" className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
> >
<ArrowDownTrayIcon className="h-5 w-5" /> <ArrowDownTrayIcon className="h-5 w-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => onChange(anexos.filter(a => a.id !== anexo.id))} onClick={() =>
onChange(anexos.filter((a) => a.id !== anexo.id))
}
className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400" className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400"
> >
<XMarkIcon className="h-5 w-5" /> <XMarkIcon className="h-5 w-5" />
+19 -11
View File
@@ -1,16 +1,20 @@
import React, { useState } from 'react'; import { format } from "date-fns";
import { useAuth } from '../../contexts/AuthContext'; import { ptBR } from "date-fns/locale";
import { Comentario } from '../../types/entities'; import React, { useState } from "react";
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale'; import { useAuth } from "../../contexts/AuthContext";
import { Comentario } from "../../types/entities";
interface ComentariosListProps { interface ComentariosListProps {
comentarios: Comentario[]; comentarios: Comentario[];
onChange: (comentarios: Comentario[]) => void; onChange: (comentarios: Comentario[]) => void;
} }
export const ComentariosList: React.FC<ComentariosListProps> = ({ comentarios, onChange }) => { export const ComentariosList: React.FC<ComentariosListProps> = ({
const [novoComentario, setNovoComentario] = useState(''); comentarios,
onChange,
}) => {
const [novoComentario, setNovoComentario] = useState("");
const { user } = useAuth(); const { user } = useAuth();
const adicionarComentario = () => { const adicionarComentario = () => {
@@ -19,13 +23,13 @@ export const ComentariosList: React.FC<ComentariosListProps> = ({ comentarios, o
const comentario: Comentario = { const comentario: Comentario = {
id: Math.random().toString(), id: Math.random().toString(),
texto: novoComentario, texto: novoComentario,
criadoPorId: user?.id || '', criadoPorId: user?.id || "",
criadoEm: new Date(), criadoEm: new Date(),
tarefaId: '' tarefaId: "",
}; };
onChange([...comentarios, comentario]); onChange([...comentarios, comentario]);
setNovoComentario(''); setNovoComentario("");
}; };
return ( return (
@@ -70,7 +74,11 @@ export const ComentariosList: React.FC<ComentariosListProps> = ({ comentarios, o
{user?.nome} {user?.nome}
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{format(new Date(comentario.criadoEm), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })} {format(
new Date(comentario.criadoEm),
"dd/MM/yyyy 'às' HH:mm",
{ locale: ptBR },
)}
</p> </p>
</div> </div>
</div> </div>
+27 -16
View File
@@ -1,14 +1,18 @@
import React, { useState } from 'react'; import { PlusIcon, XMarkIcon, CheckIcon } from "@heroicons/react/24/outline";
import { PlusIcon, XMarkIcon, CheckIcon } from '@heroicons/react/24/outline'; import React, { useState } from "react";
import { SubTarefa } from '../../types/entities';
import { SubTarefa } from "../../types/entities";
interface SubtarefasListProps { interface SubtarefasListProps {
subtarefas: SubTarefa[]; subtarefas: SubTarefa[];
onChange: (subtarefas: SubTarefa[]) => void; onChange: (subtarefas: SubTarefa[]) => void;
} }
export const SubtarefasList: React.FC<SubtarefasListProps> = ({ subtarefas, onChange }) => { export const SubtarefasList: React.FC<SubtarefasListProps> = ({
const [novaTarefa, setNovaTarefa] = useState(''); subtarefas,
onChange,
}) => {
const [novaTarefa, setNovaTarefa] = useState("");
const adicionarSubtarefa = () => { const adicionarSubtarefa = () => {
if (!novaTarefa.trim()) return; if (!novaTarefa.trim()) return;
@@ -19,10 +23,10 @@ export const SubtarefasList: React.FC<SubtarefasListProps> = ({ subtarefas, onCh
id: Math.random().toString(), id: Math.random().toString(),
titulo: novaTarefa, titulo: novaTarefa,
concluida: false, concluida: false,
tarefaId: '' tarefaId: "",
} },
]); ]);
setNovaTarefa(''); setNovaTarefa("");
}; };
return ( return (
@@ -34,7 +38,7 @@ export const SubtarefasList: React.FC<SubtarefasListProps> = ({ subtarefas, onCh
onChange={(e) => setNovaTarefa(e.target.value)} onChange={(e) => setNovaTarefa(e.target.value)}
placeholder="Nova subtarefa" placeholder="Nova subtarefa"
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
onKeyPress={(e) => e.key === 'Enter' && adicionarSubtarefa()} onKeyPress={(e) => e.key === "Enter" && adicionarSubtarefa()}
/> />
<button <button
type="button" type="button"
@@ -56,26 +60,33 @@ export const SubtarefasList: React.FC<SubtarefasListProps> = ({ subtarefas, onCh
type="button" type="button"
onClick={() => { onClick={() => {
const novasSubtarefas = [...subtarefas]; const novasSubtarefas = [...subtarefas];
novasSubtarefas[index].concluida = !novasSubtarefas[index].concluida; novasSubtarefas[index].concluida =
!novasSubtarefas[index].concluida;
onChange(novasSubtarefas); onChange(novasSubtarefas);
}} }}
className={`h-5 w-5 rounded border ${ className={`h-5 w-5 rounded border ${
subtarefa.concluida subtarefa.concluida
? 'bg-blue-600 border-blue-600 text-white' ? "bg-blue-600 border-blue-600 text-white"
: 'border-gray-300 dark:border-gray-600' : "border-gray-300 dark:border-gray-600"
}`} }`}
> >
{subtarefa.concluida && <CheckIcon className="h-4 w-4" />} {subtarefa.concluida && <CheckIcon className="h-4 w-4" />}
</button> </button>
<span className={`text-gray-900 dark:text-white ${ <span
subtarefa.concluida ? 'line-through text-gray-500 dark:text-gray-400' : '' className={`text-gray-900 dark:text-white ${
}`}> subtarefa.concluida
? "line-through text-gray-500 dark:text-gray-400"
: ""
}`}
>
{subtarefa.titulo} {subtarefa.titulo}
</span> </span>
</div> </div>
<button <button
type="button" type="button"
onClick={() => onChange(subtarefas.filter(s => s.id !== subtarefa.id))} onClick={() =>
onChange(subtarefas.filter((s) => s.id !== subtarefa.id))
}
className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400" className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400"
> >
<XMarkIcon className="h-5 w-5" /> <XMarkIcon className="h-5 w-5" />
+60 -28
View File
@@ -1,10 +1,17 @@
import React from 'react'; import {
import { Tarefa } from '../../types/entities'; PencilIcon,
import { format } from 'date-fns'; CheckIcon,
import { ptBR } from 'date-fns/locale'; XMarkIcon,
import { PencilIcon, CheckIcon, XMarkIcon, ExclamationTriangleIcon, TrashIcon } from '@heroicons/react/24/outline'; ExclamationTriangleIcon,
import { useAuth } from '../../contexts/AuthContext'; TrashIcon,
import { useTarefas } from '../../hooks/useTarefas'; } from "@heroicons/react/24/outline";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import React from "react";
import { useAuth } from "../../contexts/AuthContext";
import { useTarefas } from "../../hooks/useTarefas";
import { Tarefa } from "../../types/entities";
interface TarefaCardProps { interface TarefaCardProps {
tarefa: Tarefa; tarefa: Tarefa;
@@ -12,30 +19,39 @@ interface TarefaCardProps {
onStatusChange: (id: string, status: string, progresso?: number) => void; onStatusChange: (id: string, status: string, progresso?: number) => void;
} }
export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatusChange }) => { export const TarefaCard: React.FC<TarefaCardProps> = ({
tarefa,
onEdit,
onStatusChange,
}) => {
const { user } = useAuth(); const { user } = useAuth();
const { deleteTarefa } = useTarefas(); const { deleteTarefa } = useTarefas();
const canDelete = user && ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user.perfil); const canDelete =
user && ["ADVOGADO_ADMIN", "ADVOGADO"].includes(user.perfil);
const handleDelete = async (e: React.MouseEvent) => { const handleDelete = async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!window.confirm('Tem certeza que deseja excluir esta tarefa?')) return; if (!window.confirm("Tem certeza que deseja excluir esta tarefa?")) return;
try { try {
await deleteTarefa.mutateAsync(tarefa.id); await deleteTarefa.mutateAsync(tarefa.id);
} catch (error) { } catch (error) {
console.error('Erro ao excluir tarefa:', error); console.error("Erro ao excluir tarefa:", error);
alert('Erro ao excluir tarefa'); alert("Erro ao excluir tarefa");
} }
}; };
const getPrioridadeColor = () => { const getPrioridadeColor = () => {
switch (tarefa.prioridade) { switch (tarefa.prioridade) {
case 'BAIXA': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; case "BAIXA":
case 'MEDIA': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
case 'ALTA': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'; case "MEDIA":
case 'URGENTE': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
case "ALTA":
return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
case "URGENTE":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
} }
}; };
@@ -53,7 +69,9 @@ export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatus
> >
<PencilIcon className="h-5 w-5" /> <PencilIcon className="h-5 w-5" />
</button> </button>
<span className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor()}`}> <span
className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor()}`}
>
{tarefa.prioridade} {tarefa.prioridade}
</span> </span>
</div> </div>
@@ -79,16 +97,24 @@ export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatus
<div <div
key={subtarefa.id} key={subtarefa.id}
className={`h-6 w-6 rounded-full flex items-center justify-center text-xs className={`h-6 w-6 rounded-full flex items-center justify-center text-xs
${subtarefa.concluida ${
? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-200' subtarefa.concluida
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'}`} ? "bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-200"
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
}`}
> >
{subtarefa.concluida ? <CheckIcon className="h-3 w-3" /> : index + 1} {subtarefa.concluida ? (
<CheckIcon className="h-3 w-3" />
) : (
index + 1
)}
</div> </div>
))} ))}
</div> </div>
<span className="text-gray-500 dark:text-gray-400"> <span className="text-gray-500 dark:text-gray-400">
{format(new Date(tarefa.dataVencimento), "dd/MM/yyyy", { locale: ptBR })} {format(new Date(tarefa.dataVencimento), "dd/MM/yyyy", {
locale: ptBR,
})}
</span> </span>
</div> </div>
@@ -105,17 +131,17 @@ export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatus
<div className="mt-4 flex items-center justify-between"> <div className="mt-4 flex items-center justify-between">
<div className="flex space-x-2"> <div className="flex space-x-2">
{tarefa.status !== 'CONCLUIDA' && tarefa.status !== 'CANCELADA' && ( {tarefa.status !== "CONCLUIDA" && tarefa.status !== "CANCELADA" && (
<> <>
<button <button
onClick={() => onStatusChange(tarefa.id, 'CONCLUIDA', 100)} onClick={() => onStatusChange(tarefa.id, "CONCLUIDA", 100)}
className="p-2 text-green-600 hover:bg-green-50 rounded-lg" className="p-2 text-green-600 hover:bg-green-50 rounded-lg"
title="Concluir tarefa" title="Concluir tarefa"
> >
<CheckIcon className="h-5 w-5" /> <CheckIcon className="h-5 w-5" />
</button> </button>
<button <button
onClick={() => onStatusChange(tarefa.id, 'CANCELADA', 0)} onClick={() => onStatusChange(tarefa.id, "CANCELADA", 0)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg" className="p-2 text-red-600 hover:bg-red-50 rounded-lg"
title="Cancelar tarefa" title="Cancelar tarefa"
> >
@@ -123,10 +149,16 @@ export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatus
</button> </button>
<button <button
onClick={() => { onClick={() => {
const novoProgresso = prompt('Digite o percentual de conclusão (0-100):', tarefa.progresso?.toString()); const novoProgresso = prompt(
"Digite o percentual de conclusão (0-100):",
tarefa.progresso?.toString(),
);
if (novoProgresso !== null) { if (novoProgresso !== null) {
const progresso = Math.min(100, Math.max(0, parseInt(novoProgresso))); const progresso = Math.min(
onStatusChange(tarefa.id, 'EM_ANDAMENTO', progresso); 100,
Math.max(0, parseInt(novoProgresso)),
);
onStatusChange(tarefa.id, "EM_ANDAMENTO", progresso);
} }
}} }}
className="p-2 text-yellow-600 hover:bg-yellow-50 rounded-lg" className="p-2 text-yellow-600 hover:bg-yellow-50 rounded-lg"
+21 -13
View File
@@ -1,9 +1,11 @@
import React from 'react'; import React from "react";
import { useQuery } from 'react-query'; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import { useQuery } from "react-query";
import { Tarefa } from '../../types/entities';
import { TarefaCard } from './TarefaCard'; import api from "../../services/api";
import api from '../../services/api'; import { Tarefa } from "../../types/entities";
import { TarefaCard } from "./TarefaCard";
interface TarefaKanbanProps { interface TarefaKanbanProps {
onEdit: (tarefa: Tarefa) => void; onEdit: (tarefa: Tarefa) => void;
@@ -11,14 +13,17 @@ interface TarefaKanbanProps {
} }
const colunas = [ const colunas = [
{ id: 'PENDENTE', titulo: 'Pendentes' }, { id: "PENDENTE", titulo: "Pendentes" },
{ id: 'EM_ANDAMENTO', titulo: 'Em Andamento' }, { id: "EM_ANDAMENTO", titulo: "Em Andamento" },
{ id: 'CONCLUIDA', titulo: 'Concluídas' }, { id: "CONCLUIDA", titulo: "Concluídas" },
]; ];
export const TarefaKanban: React.FC<TarefaKanbanProps> = ({ onEdit, onStatusChange }) => { export const TarefaKanban: React.FC<TarefaKanbanProps> = ({
const { data: tarefas = [], refetch } = useQuery('tarefas', async () => { onEdit,
const response = await api.get('/tarefas'); onStatusChange,
}) => {
const { data: tarefas = [], refetch } = useQuery("tarefas", async () => {
const response = await api.get("/tarefas");
return response.data; return response.data;
}); });
@@ -36,7 +41,10 @@ export const TarefaKanban: React.FC<TarefaKanbanProps> = ({ onEdit, onStatusChan
<DragDropContext onDragEnd={handleDragEnd}> <DragDropContext onDragEnd={handleDragEnd}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{colunas.map((coluna) => ( {colunas.map((coluna) => (
<div key={coluna.id} className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4"> <div
key={coluna.id}
className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4"
>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
{coluna.titulo} {coluna.titulo}
</h3> </h3>
+66 -52
View File
@@ -1,14 +1,16 @@
import React, { useState, useEffect } from 'react'; import { Dialog } from "@headlessui/react";
import { Dialog } from '@headlessui/react'; import { XMarkIcon } from "@heroicons/react/24/outline";
import { useForm } from 'react-hook-form'; import { DatePicker } from "@mui/x-date-pickers";
import { useQuery } from 'react-query'; import React, { useState, useEffect } from "react";
import { DatePicker } from '@mui/x-date-pickers'; import { useForm } from "react-hook-form";
import { Tarefa } from '../../types/entities'; import { useQuery } from "react-query";
import { XMarkIcon } from '@heroicons/react/24/outline';
import { SubtarefasList } from './SubtarefasList'; import { useTarefas } from "../../hooks/useTarefas";
import { ComentariosList } from './ComentariosList'; import api from "../../services/api";
import api from '../../services/api'; import { Tarefa } from "../../types/entities";
import { useTarefas } from '../../hooks/useTarefas';
import { ComentariosList } from "./ComentariosList";
import { SubtarefasList } from "./SubtarefasList";
interface TarefaModalProps { interface TarefaModalProps {
isOpen: boolean; isOpen: boolean;
@@ -16,21 +18,25 @@ interface TarefaModalProps {
tarefa?: Tarefa | null; tarefa?: Tarefa | null;
} }
export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, tarefa }) => { export const TarefaModal: React.FC<TarefaModalProps> = ({
const [activeTab, setActiveTab] = useState('detalhes'); isOpen,
onClose,
tarefa,
}) => {
const [activeTab, setActiveTab] = useState("detalhes");
const { register, handleSubmit, reset, setValue, watch } = useForm(); const { register, handleSubmit, reset, setValue, watch } = useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { createTarefa, updateTarefa } = useTarefas(); const { createTarefa, updateTarefa } = useTarefas();
// Buscar usuários para atribuição // Buscar usuários para atribuição
const { data: usuarios = [] } = useQuery('usuarios', async () => { const { data: usuarios = [] } = useQuery("usuarios", async () => {
const response = await api.get('/usuarios'); const response = await api.get("/usuarios");
return response.data; return response.data;
}); });
// Buscar processos ativos // Buscar processos ativos
const { data: processos = [] } = useQuery('processos', async () => { const { data: processos = [] } = useQuery("processos", async () => {
const response = await api.get('/processos'); const response = await api.get("/processos");
return response.data; return response.data;
}); });
@@ -39,12 +45,12 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
reset(tarefa); reset(tarefa);
} else { } else {
reset({ reset({
status: 'PENDENTE', status: "PENDENTE",
prioridade: 'MEDIA', prioridade: "MEDIA",
progresso: 0, progresso: 0,
etiquetas: [], etiquetas: [],
subtarefas: [], subtarefas: [],
comentarios: [] comentarios: [],
}); });
} }
}, [tarefa, reset]); }, [tarefa, reset]);
@@ -54,7 +60,7 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
reset({ reset({
...tarefa, ...tarefa,
dataInicio: new Date(tarefa.dataInicio), dataInicio: new Date(tarefa.dataInicio),
dataVencimento: new Date(tarefa.dataVencimento) dataVencimento: new Date(tarefa.dataVencimento),
}); });
} }
}, [tarefa, isOpen, reset]); }, [tarefa, isOpen, reset]);
@@ -70,31 +76,31 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
// Remover campos que não devem ser enviados // Remover campos que não devem ser enviados
id: undefined, id: undefined,
criadoEm: undefined, criadoEm: undefined,
atualizadoEm: undefined atualizadoEm: undefined,
}; };
if (tarefa) { if (tarefa) {
// Para edição, enviar id separadamente // Para edição, enviar id separadamente
await updateTarefa.mutateAsync({ await updateTarefa.mutateAsync({
id: tarefa.id, id: tarefa.id,
data: dadosParaEnviar data: dadosParaEnviar,
}); });
} else { } else {
await createTarefa.mutateAsync(dadosParaEnviar); await createTarefa.mutateAsync(dadosParaEnviar);
} }
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Erro ao salvar tarefa:', error); console.error("Erro ao salvar tarefa:", error);
alert('Erro ao salvar tarefa. Por favor, tente novamente.'); alert("Erro ao salvar tarefa. Por favor, tente novamente.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const tabs = [ const tabs = [
{ id: 'detalhes', label: 'Detalhes' }, { id: "detalhes", label: "Detalhes" },
{ id: 'subtarefas', label: 'Subtarefas' }, { id: "subtarefas", label: "Subtarefas" },
{ id: 'comentarios', label: 'Comentários' }, { id: "comentarios", label: "Comentários" },
]; ];
return ( return (
@@ -108,23 +114,27 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
<div className="border-b border-gray-200 dark:border-gray-700 p-4"> <div className="border-b border-gray-200 dark:border-gray-700 p-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Dialog.Title className="text-lg font-semibold text-gray-900 dark:text-white"> <Dialog.Title className="text-lg font-semibold text-gray-900 dark:text-white">
{tarefa ? 'Editar Tarefa' : 'Nova Tarefa'} {tarefa ? "Editar Tarefa" : "Nova Tarefa"}
</Dialog.Title> </Dialog.Title>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"> <button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<XMarkIcon className="h-6 w-6" /> <XMarkIcon className="h-6 w-6" />
</button> </button>
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex space-x-4 mt-4"> <div className="flex space-x-4 mt-4">
{tabs.map(tab => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors
${activeTab === tab.id ${
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200' activeTab === tab.id
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200' ? "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
}`} }`}
> >
{tab.label} {tab.label}
@@ -137,12 +147,12 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
<form onSubmit={handleSubmit(onSubmit)} className="p-6"> <form onSubmit={handleSubmit(onSubmit)} className="p-6">
{/* Conteúdo das tabs */} {/* Conteúdo das tabs */}
<div className="space-y-6"> <div className="space-y-6">
{activeTab === 'detalhes' && ( {activeTab === "detalhes" && (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="col-span-2"> <div className="col-span-2">
<input <input
type="text" type="text"
{...register('titulo')} {...register("titulo")}
placeholder="Título da tarefa" placeholder="Título da tarefa"
className="w-full px-4 py-2 text-xl font-medium border-0 border-b-2 border-gray-200 dark:border-gray-700 bg-transparent text-gray-900 dark:text-white focus:ring-0 focus:border-blue-500" className="w-full px-4 py-2 text-xl font-medium border-0 border-b-2 border-gray-200 dark:border-gray-700 bg-transparent text-gray-900 dark:text-white focus:ring-0 focus:border-blue-500"
/> />
@@ -150,7 +160,7 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
<div className="col-span-2"> <div className="col-span-2">
<textarea <textarea
{...register('descricao')} {...register("descricao")}
rows={3} rows={3}
placeholder="Descrição detalhada" placeholder="Descrição detalhada"
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
@@ -159,7 +169,7 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
<div className="space-y-4"> <div className="space-y-4">
<select <select
{...register('prioridade')} {...register("prioridade")}
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
> >
<option value="BAIXA">Baixa</option> <option value="BAIXA">Baixa</option>
@@ -169,7 +179,7 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
</select> </select>
<select <select
{...register('responsavelId')} {...register("responsavelId")}
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
> >
<option value="">Selecione um responsável</option> <option value="">Selecione um responsável</option>
@@ -184,34 +194,38 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
<div className="space-y-4"> <div className="space-y-4">
<DatePicker <DatePicker
label="Data de Início" label="Data de Início"
value={watch('dataInicio')} value={watch("dataInicio")}
onChange={(date) => setValue('dataInicio', date)} onChange={(date) => setValue("dataInicio", date)}
className="w-full" className="w-full"
slotProps={{ textField: { size: 'small' } }} slotProps={{ textField: { size: "small" } }}
/> />
<DatePicker <DatePicker
label="Data de Vencimento" label="Data de Vencimento"
value={watch('dataVencimento')} value={watch("dataVencimento")}
onChange={(date) => setValue('dataVencimento', date)} onChange={(date) => setValue("dataVencimento", date)}
className="w-full" className="w-full"
slotProps={{ textField: { size: 'small' } }} slotProps={{ textField: { size: "small" } }}
/> />
</div> </div>
</div> </div>
)} )}
{activeTab === 'subtarefas' && ( {activeTab === "subtarefas" && (
<SubtarefasList <SubtarefasList
subtarefas={watch('subtarefas') || []} subtarefas={watch("subtarefas") || []}
onChange={(subtarefas) => setValue('subtarefas', subtarefas)} onChange={(subtarefas) =>
setValue("subtarefas", subtarefas)
}
/> />
)} )}
{activeTab === 'comentarios' && ( {activeTab === "comentarios" && (
<ComentariosList <ComentariosList
comentarios={watch('comentarios') || []} comentarios={watch("comentarios") || []}
onChange={(comentarios) => setValue('comentarios', comentarios)} onChange={(comentarios) =>
setValue("comentarios", comentarios)
}
/> />
)} )}
</div> </div>
@@ -229,7 +243,7 @@ export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, taref
disabled={loading} disabled={loading}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50"
> >
{loading ? 'Salvando...' : tarefa ? 'Salvar' : 'Criar'} {loading ? "Salvando..." : tarefa ? "Salvar" : "Criar"}
</button> </button>
</div> </div>
</form> </form>
+63 -34
View File
@@ -1,53 +1,58 @@
import React from 'react'; import { format } from "date-fns";
import { useQuery } from 'react-query'; import { ptBR } from "date-fns/locale";
import { Tarefa } from '../../types/entities'; import React from "react";
import { format } from 'date-fns'; import { useQuery } from "react-query";
import { ptBR } from 'date-fns/locale';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { useAuth } from "../../contexts/AuthContext";
import api from '../../services/api'; import { useTarefas } from "../../hooks/useTarefas";
import { useAuth } from '../../contexts/AuthContext'; import { Tarefa } from "../../types/entities";
import { useTarefas } from '../../hooks/useTarefas';
interface TarefasListProps { interface TarefasListProps {
onEdit: (tarefa: Tarefa) => void; onEdit: (tarefa: Tarefa) => void;
onStatusChange: (id: string, status: string, progresso?: number) => void; onStatusChange: (id: string, status: string, progresso?: number) => void;
} }
export const TarefasList: React.FC<TarefasListProps> = ({ onEdit, onStatusChange }) => { export const TarefasList: React.FC<TarefasListProps> = ({
onEdit,
onStatusChange,
}) => {
const { user } = useAuth(); const { user } = useAuth();
const { tarefas, isLoading, deleteTarefa } = useTarefas(); const { tarefas, isLoading, deleteTarefa } = useTarefas();
const getPrioridadeColor = (prioridade: string) => { const getPrioridadeColor = (prioridade: string) => {
const colors = { const colors = {
'BAIXA': 'bg-green-100 text-green-800', BAIXA: "bg-green-100 text-green-800",
'MEDIA': 'bg-yellow-100 text-yellow-800', MEDIA: "bg-yellow-100 text-yellow-800",
'ALTA': 'bg-orange-100 text-orange-800', ALTA: "bg-orange-100 text-orange-800",
'URGENTE': 'bg-red-100 text-red-800' URGENTE: "bg-red-100 text-red-800",
}; };
return colors[prioridade as keyof typeof colors] || 'bg-gray-100 text-gray-800'; return (
colors[prioridade as keyof typeof colors] || "bg-gray-100 text-gray-800"
);
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const colors = { const colors = {
'PENDENTE': 'bg-gray-100 text-gray-800', PENDENTE: "bg-gray-100 text-gray-800",
'EM_ANDAMENTO': 'bg-blue-100 text-blue-800', EM_ANDAMENTO: "bg-blue-100 text-blue-800",
'CONCLUIDA': 'bg-green-100 text-green-800', CONCLUIDA: "bg-green-100 text-green-800",
'CANCELADA': 'bg-red-100 text-red-800' CANCELADA: "bg-red-100 text-red-800",
}; };
return colors[status as keyof typeof colors] || 'bg-gray-100 text-gray-800'; return colors[status as keyof typeof colors] || "bg-gray-100 text-gray-800";
}; };
const canDelete = user && ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user.perfil); const canDelete =
user && ["ADVOGADO_ADMIN", "ADVOGADO"].includes(user.perfil);
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (!window.confirm('Tem certeza que deseja excluir esta tarefa?')) return; if (!window.confirm("Tem certeza que deseja excluir esta tarefa?")) return;
try { try {
await deleteTarefa.mutateAsync(id); await deleteTarefa.mutateAsync(id);
// Não precisa fazer nada aqui, o React Query vai atualizar automaticamente // Não precisa fazer nada aqui, o React Query vai atualizar automaticamente
} catch (error) { } catch (error) {
console.error('Erro ao excluir tarefa:', error); console.error("Erro ao excluir tarefa:", error);
alert('Erro ao excluir tarefa'); alert("Erro ao excluir tarefa");
} }
}; };
@@ -56,19 +61,34 @@ export const TarefasList: React.FC<TarefasListProps> = ({ onEdit, onStatusChange
<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 scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Título/Descrição Título/Descrição
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Status Status
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Prioridade Prioridade
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Vencimento Vencimento
</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"> <th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Progresso Progresso
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
@@ -78,7 +98,10 @@ export const TarefasList: React.FC<TarefasListProps> = ({ onEdit, onStatusChange
</thead> </thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{tarefas.map((tarefa: Tarefa) => ( {tarefas.map((tarefa: Tarefa) => (
<tr key={tarefa.id} className="hover:bg-gray-50 dark:hover:bg-gray-700"> <tr
key={tarefa.id}
className="hover:bg-gray-50 dark:hover:bg-gray-700"
>
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="text-sm font-medium text-gray-900 dark:text-white"> <div className="text-sm font-medium text-gray-900 dark:text-white">
{tarefa.titulo} {tarefa.titulo}
@@ -88,18 +111,24 @@ export const TarefasList: React.FC<TarefasListProps> = ({ onEdit, onStatusChange
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(tarefa.status)}`}> <span
{tarefa.status.replace('_', ' ')} className={`px-2 py-1 text-xs rounded-full ${getStatusColor(tarefa.status)}`}
>
{tarefa.status.replace("_", " ")}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor(tarefa.prioridade)}`}> <span
className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor(tarefa.prioridade)}`}
>
{tarefa.prioridade} {tarefa.prioridade}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(tarefa.dataVencimento), 'dd/MM/yyyy', { locale: ptBR })} {format(new Date(tarefa.dataVencimento), "dd/MM/yyyy", {
locale: ptBR,
})}
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff. Show more