Compare commits

...

17 Commits

Author SHA1 Message Date
a4d7fae123 Merge branch 'main' into ExcecoesFinal 2025-10-15 21:35:39 -03:00
d08aec9363 Merge branch 'main' into ExcecoesFinal 2025-10-15 21:07:54 -03:00
25964088af resolução de erros 2025-10-15 20:30:04 -03:00
1955dbfd1e tiptapperfilmerge 2025-10-15 20:24:02 -03:00
610a1cca3a tiptapperfilmerge 2025-10-15 20:21:40 -03:00
jp-lima
bba034c518 Merge branch 'AgendamentoConsultas' 2025-10-15 20:14:06 -03:00
7f14cf16b7 tiptap 2025-10-15 20:14:03 -03:00
f4f4f5f275 revert 2df76ca04dd80fcd65900a95ea64fbc100724196
revert Merge pull request 'API-Disponibilidade' (#6) from API-Disponibilidade into main

Reviewed-on: #6
2025-10-15 23:09:35 +00:00
2df76ca04d Merge pull request 'API-Disponibilidade' (#6) from API-Disponibilidade into main
Reviewed-on: #6
2025-10-15 22:52:49 +00:00
jp-lima
13e4064989 Melhorias no estilo 2025-10-15 16:47:52 -03:00
1c4512511c perfil paciente 2025-10-15 15:17:56 -03:00
jp-lima
cc37ea60da Finalização das agendas 2025-10-15 09:34:32 -03:00
jp-lima
39a78db8c5 Função do agendamento GET para semana 2025-10-13 15:49:38 -03:00
97840ebed4 mudanças2 2025-10-12 15:40:07 -03:00
jp-lima
b949971a28 Começo da criação de mostrar as consultas separadas por dias 2025-10-10 15:48:08 -03:00
3833764438 loginperfil1 2025-10-10 15:36:32 -03:00
jp-lima
ffea77d911 POST de agendamentos 2025-10-10 13:15:30 -03:00
36 changed files with 2789 additions and 1638 deletions

746
package-lock.json generated
View File

@ -15,6 +15,9 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@tiptap/extension-placeholder": "^3.7.1",
"@tiptap/react": "^3.7.1",
"@tiptap/starter-kit": "^3.7.1",
"apexcharts": "^5.3.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
@ -16184,6 +16187,31 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"optional": true,
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"optional": true,
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"optional": true
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -17174,6 +17202,11 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/@remirror/core-constants": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="
},
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
@ -17747,6 +17780,424 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tiptap/core": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.7.1.tgz",
"integrity": "sha512-jB6R8EGI34QUmV7EhtE+JVpjbZ6Wa0dcf0LNS36X9V7FtDQcnxl7ekRs/ftELt/6qOjubRdyhaID0wNdJVmFtw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.7.1.tgz",
"integrity": "sha512-UPIne4kD8hwhadPtapn0WfJCNiF+b3ftNYiC1BpNfti5NmM0sXuqOOC0WnVgGgsNuJp4hd+4PMp42InlD6/1aw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.7.1.tgz",
"integrity": "sha512-XZRt1blYGpqVlcBo+PKH1mlbsqdc5KsWi/ZsPBV3Ajg/Vx5d6SAY4wK6CW1SpotE1wWucUhfAmXddhBFvYzaUA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.7.1.tgz",
"integrity": "sha512-7qLK49GC7pW6FbAE6vOGphcyjq7CqiBEwr9i9/5UgnadjLtREDzBl28D+95+8TkyF7sM3hP6s6RU+nh87v5fqw==",
"optional": true,
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.7.1.tgz",
"integrity": "sha512-AO7EVAftvzSw7Sftp36P+HNedxjygMpobYNTBQzHfGljRZh8VDhIUzwyP1OsmlrcCbBxsrjMZLrmk/ozsALq0g==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "^3.7.1"
}
},
"node_modules/@tiptap/extension-code": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.7.1.tgz",
"integrity": "sha512-ZRarYvgQ16ZrzKox/iW3bVr5IVNBsD0yjU5S7GVmlRgRQ8lhsTloLk9Gu05uuZ6dOoL3qApLA8+W7w8sxZJ35w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.7.1.tgz",
"integrity": "sha512-/Ov81QXEn6AOiiSUFlM57a+YSye/Lkhvgy303+CEGtDuFVU/SJ0tDsgmSYzkP5q6DIVQLAXp5WkxEo02GnYHgQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-document": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.7.1.tgz",
"integrity": "sha512-b7NHWseJSvhhbsiSWjQgiJcs6FUJiEJocfhazDiWAOk5ELQ6+oiIe7ecEgDqBmafk9oziV9r7u9OAgyeyP3JBA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.7.1.tgz",
"integrity": "sha512-wZT3bPeNJAasOvNr6tUZAwXFeKlQEToSnVAjFiBzJwLDonuK8ZaAiBCDQgqEQSlP3HsEE4/qkERBNrdyAT26CQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "^3.7.1"
}
},
"node_modules/@tiptap/extension-floating-menu": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.7.1.tgz",
"integrity": "sha512-epfA87IIBy5IREMjmlRskp8T/9/avjfM8RtcqDnKQxVVXn8yl5i0Pca0jXD4w7rIAS7G95N9sYRxsou6Y6fTQg==",
"optional": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.7.1.tgz",
"integrity": "sha512-1UrZEaqruWPLdgYsAm4au7BAyTDjaNRP0E7UIoEoGsq+MAS2MM3g4suXMzu+l3ZIayrSy98N3T8DIUG+U6+mww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "^3.7.1"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.7.1.tgz",
"integrity": "sha512-pEvRjWexMNxXH5FOy3EhzyMFDFHrRTWOgZbWAxliKDg2dFEJ50e9KcCMDs87e7++V753lEKnFTmz/9WaH7cwcQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.7.1.tgz",
"integrity": "sha512-rOUou6b0+5E+DAmEMTC/mlKTLiOr4D0LKzBfqBLQ3zUyZPZabOKzN0L+4MaLNR2CkXy/Ae4du5ucHGrGOWzVrQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.7.1.tgz",
"integrity": "sha512-f4lXW/LHuJBF11PIrWdNAzTmlapV4fVujJ5eCsLAkpzhx3izVrDW/WlKRrkGUCy/qQT4v7BbHNa5JYlKDzDo0Q==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.7.1.tgz",
"integrity": "sha512-Bm6eOtcafc5kjE357GlvIY2hNTRRAkb8D5SRm8zYlVB0fiLto+r15Ht+DTOmLiQKEGtEArQ/C8Rh2j09UdH2vA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-link": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.7.1.tgz",
"integrity": "sha512-6+0/mo+EKDiA1d1pDZSsf/51ZOwdFnN35yF/4celxdr/JL4aupvtttIjGAtWd37h50cadYSL4F1uacKs7yyh8Q==",
"dependencies": {
"linkifyjs": "^4.3.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-list": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.7.1.tgz",
"integrity": "sha512-E93oXkV2vsZThsix0OA7RiHNLIMGi+w9ASKZ+8TGB69oy32yujnnZz6YVhTVVDPOw8rCP5CnOPhJbgdcqByr0A==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.7.1.tgz",
"integrity": "sha512-qkXfWRBusJCId9VhRo9vihcrmxvJ83fkzYWI0LiefJCT1LKfMaeInFNxIsFeUU4q9nR0mhZo7ES3E2+Tk0U3Mw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "^3.7.1"
}
},
"node_modules/@tiptap/extension-list-keymap": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.7.1.tgz",
"integrity": "sha512-3WyzWge/g6FoxMTkoAARtMJyIYQbpclNX48HyAqdwjJXuLmz3qckEnJEXo47CvJlRsNAlcDJniRS9j5SVJupRw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "^3.7.1"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.7.1.tgz",
"integrity": "sha512-iX3DhTwFp84fiCNSF7+kl/sq6orXq2QFcV2AH+CvL+d0WW1STYmmVmE26gHEjyY82QfpvLZYUCEG6RSYpxFIZw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "^3.7.1"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.7.1.tgz",
"integrity": "sha512-L5dsppKKo46MN3Go5vzqqzjPX89pz1lIkIUN3IhU+KmAHg1TklfR7FQkiIFIIV2rb2ZLuLpD/JcNsZAUmJTW5Q==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-placeholder": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.7.1.tgz",
"integrity": "sha512-35VZ578c9oho62Cpupzo9rZCxhPNtuLHNnpKy7bXYaR0Quy3/axtnly1x09nS/KZ4oqw8rvYtkS6GAXwMKKtLg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "^3.7.1"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.7.1.tgz",
"integrity": "sha512-Ctqk/SfmGd3hFCDr4/OH0Dnja19UWUrUEY62pwM7JCkbY/Y9QwPLSO32L6KyamwUDek9SL/ATjRPz6GLp0P7hg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-text": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.7.1.tgz",
"integrity": "sha512-m+8FJrFAllJYuzLbEXJ9AztobxmWBTjWorkHcMHBLAbY2ytmAhIM1u3ExtOn9DjvnIT6MffCaq0i/KjhSBYJlA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extension-underline": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.7.1.tgz",
"integrity": "sha512-tyx7ZM2ll8DclKe9Ea/vPyqaZBgnJfIbKBOpecpzawDaJ5ocjwywmYNduevOhw327X2/i8LIQBsPuIOJselcUQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1"
}
},
"node_modules/@tiptap/extensions": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.7.1.tgz",
"integrity": "sha512-O7eq3frqh7kn/J2P+lpx8blBQrIQxt21J3NvlQJhW5nXIECdo2ox8SQcEfli0EqMSwZCZTdVufdFBkWfIRXhRg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1"
}
},
"node_modules/@tiptap/pm": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.7.1.tgz",
"integrity": "sha512-t3n054kplRtRYn8pDnzF/prDUccF7QX7jPYLsYBpLn3+d59J5KKkBmOpPExUGE8kZkNoLfwffAFj6NBfqOu+Xg==",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.24.1",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.38.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/react": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.7.1.tgz",
"integrity": "sha512-0tDih8ACrVhKCpNjHcQhxV5DBsyY4xpAjr8nfb2RK+J2l1lGtNuj3PDRHlDLroQiK8svwARz4hsBs8MYWKSkfg==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"fast-deep-equal": "^3.1.3",
"use-sync-external-store": "^1.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.7.1",
"@tiptap/extension-floating-menu": "^3.7.1"
},
"peerDependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/pm": "^3.7.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.7.1.tgz",
"integrity": "sha512-ZYgA3BkASQmHyoDlUYKFPEpCzIcn/FP/Sb+Ic2L7gt2gOC7zvWAVc/2yIbiFuq+48s+5U/KJqDiXn2hiLwXxpA==",
"dependencies": {
"@tiptap/core": "^3.7.1",
"@tiptap/extension-blockquote": "^3.7.1",
"@tiptap/extension-bold": "^3.7.1",
"@tiptap/extension-bullet-list": "^3.7.1",
"@tiptap/extension-code": "^3.7.1",
"@tiptap/extension-code-block": "^3.7.1",
"@tiptap/extension-document": "^3.7.1",
"@tiptap/extension-dropcursor": "^3.7.1",
"@tiptap/extension-gapcursor": "^3.7.1",
"@tiptap/extension-hard-break": "^3.7.1",
"@tiptap/extension-heading": "^3.7.1",
"@tiptap/extension-horizontal-rule": "^3.7.1",
"@tiptap/extension-italic": "^3.7.1",
"@tiptap/extension-link": "^3.7.1",
"@tiptap/extension-list": "^3.7.1",
"@tiptap/extension-list-item": "^3.7.1",
"@tiptap/extension-list-keymap": "^3.7.1",
"@tiptap/extension-ordered-list": "^3.7.1",
"@tiptap/extension-paragraph": "^3.7.1",
"@tiptap/extension-strike": "^3.7.1",
"@tiptap/extension-text": "^3.7.1",
"@tiptap/extension-underline": "^3.7.1",
"@tiptap/extensions": "^3.7.1",
"@tiptap/pm": "^3.7.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -18078,6 +18529,20 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"license": "MIT"
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -18088,6 +18553,11 @@
"@types/unist": "*"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -18184,14 +18654,22 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"license": "MIT",
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@ -21060,6 +21538,11 @@
"node": ">=10"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -26850,6 +27333,19 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/linkifyjs": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@ -27041,6 +27537,38 @@
"tmpl": "1.0.5"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@ -27329,6 +27857,11 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"license": "CC0-1.0"
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -28526,6 +29059,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
},
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@ -30322,6 +30860,183 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/prosemirror-changeset": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
"integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
"dependencies": {
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-collab": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
"dependencies": {
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-commands": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-dropcursor": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"node_modules/prosemirror-history": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
"integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-inputrules": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-markdown": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
"integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
"dependencies": {
"@types/markdown-it": "^14.0.0",
"markdown-it": "^14.0.0",
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-menu": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
"integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
"dependencies": {
"crelt": "^1.0.0",
"prosemirror-commands": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.3.tgz",
"integrity": "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-schema-basic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
"dependencies": {
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz",
"integrity": "sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==",
"dependencies": {
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.25.0",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.3",
"prosemirror-view": "^1.39.1"
}
},
"node_modules/prosemirror-trailing-node": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
"dependencies": {
"@remirror/core-constants": "3.0.0",
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
"prosemirror-model": "^1.22.1",
"prosemirror-state": "^1.4.2",
"prosemirror-view": "^1.33.8"
}
},
"node_modules/prosemirror-transform": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz",
"integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.41.3",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz",
"integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -30365,6 +31080,14 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"engines": {
"node": ">=6"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -31707,6 +32430,11 @@
"randombytes": "^2.1.0"
}
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -33802,6 +34530,11 @@
"node": ">=4.2.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@ -34250,6 +34983,11 @@
"browser-process-hrtime": "^1.0.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",

View File

@ -10,6 +10,9 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@tiptap/extension-placeholder": "^3.7.1",
"@tiptap/react": "^3.7.1",
"@tiptap/starter-kit": "^3.7.1",
"apexcharts": "^5.3.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",

View File

@ -10,6 +10,11 @@ import PerfilFinanceiro from "./perfis/perfil_financeiro/PerfilFinanceiro";
import Perfiladm from "./perfis/Perfil_adm/Perfiladm";
import PerfilMedico from "./perfis/Perfil_medico/PerfilMedico";
// COMBINADO: Importações de ambas as versões
import PerfilPaciente from "./perfis/Perfil_paciente/Perfilpaciente"
import ProfilePage from "./pages/ProfilePage";
import Header from "./components/Header/Header";
// Componentes globais de acessibilidade
import VlibrasWidget from "./components/VlibrasWidget";
@ -20,6 +25,7 @@ function App() {
<Router>
<VlibrasWidget />
<BotaoAcessibilidade />
<Header />
<Routes>
<Route path="/" element={<LandingPage />} />
@ -30,6 +36,11 @@ function App() {
<Route path="/financeiro/*" element={<PerfilFinanceiro />} />
<Route path="/medico/*" element={<PerfilMedico />} />
<Route path="/admin/*" element={<Perfiladm />} />
{/* COMBINADO: Rotas de ambas as versões */}
<Route path="/paciente/*" element={<PerfilPaciente />} />
<Route path="/perfil" element={<ProfilePage />} />
<Route path="*" element={<h2>Página não encontrada</h2>} />
</Routes>
</Router>
@ -37,4 +48,3 @@ function App() {
}
export default App;

View File

@ -5,6 +5,8 @@ import { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer';
const DoctorRelatorioManager = () => {
const navigate = useNavigate()
const {getAuthorizationHeader} = useAuth();
@ -13,7 +15,7 @@ const DoctorRelatorioManager = () => {
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([])
const [showModal, setShowModal] = useState(false)
const [index, setIndex] = useState()
// 1º useEffect: Busca os dados dos pacientes após carregar os relatórios
useEffect( () => {
let pacientesDosRelatorios = []
@ -26,34 +28,32 @@ const DoctorRelatorioManager = () => {
if (paciente.length > 0) {
pacientesDosRelatorios.push(paciente[0]);
}
}
setPacientesComRelatorios(pacientesDosRelatorios);
}
ListarPacientes()
}, [RelatoriosFiltrados, authHeader]);
// NOVO: useEffect para logar PacientesComRelatorios após a atualização
useEffect(() => {
console.log(PacientesComRelatorios, 'aqui')
}, [PacientesComRelatorios])
}, [RelatoriosFiltrados]);
// 2º useEffect: Busca a lista de relatórios
useEffect(() => {
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&status", requestOptions)
.then(response => response.json())
.then(data => { setRelatorios(data); console.log(data) })
.catch(error => console.log('error', error));
}, [])
}, [authHeader])
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = {
@ -62,10 +62,8 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
html2pdf().set(opt).from(elemento).save();
}
return (
<div>
{showModal && (
@ -82,36 +80,36 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</div>
<div className="modal-body">
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {PacientesComRelatorios[index]?.full_name}</p>
<p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date} </p>
<p>Data do exame: {}</p>
<p>Exame: {RelatoriosFiltrados[index]?.exam}</p>
<p>Diagnostico: {RelatoriosFiltrados[index]?.diagnosis}</p>
<p>Conclusão: {RelatoriosFiltrados[index]?.conclusion}</p>
{/* INÍCIO DA MUDANÇA (da resposta anterior) */}
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<TiptapViewer
htmlContent={
RelatoriosFiltrados[index]?.content ||
RelatoriosFiltrados[index]?.diagnosis ||
RelatoriosFiltrados[index]?.conclusion ||
'Relatório não preenchido.'
}
/>
{/* FIM DA MUDANÇA */}
</div>
<div>
<p>Dr {RelatoriosFiltrados[index]?.required_by}</p>
<p>Emitido em: 0</p>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button
type="button"
className="btn btn-primary"
@ -124,8 +122,6 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</div>
</div>
)}
<div className="page-heading">
<h3>Lista de Relatórios</h3>
</div>
@ -143,14 +139,12 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</button>
</Link>
</div>
<div className="card-body">
<div className="card p-3 mb-3">
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>{" "}
Filtros
</h5>
<div
className="d-flex flex-nowrap align-items-center gap-2"
style={{ overflowX: "auto", paddingBottom: "6px" }}
@ -170,7 +164,6 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
/>
</div>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead>
@ -178,7 +171,6 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
<th>Paciente</th>
<th>CPF</th>
<th>Exame</th>
<th></th>
@ -211,7 +203,6 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</button>
<button
className="btn btn-sm"
style={{
@ -220,6 +211,7 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
}}
onClick={() => {
// MANTIDO: Uso de string template para a navegação
navigate(`/medico/relatorios/${relatorio.id}/edit`)
}}
>
@ -247,10 +239,7 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</div>
</section>
</div>
</div>
)
}
export default DoctorRelatorioManager

View File

@ -1,63 +1,170 @@
import React, { useEffect, useState } from 'react'
import FormRelatorio from '../components/FormRelatorio'
import { useParams } from 'react-router-dom'
import { useParams, useNavigate } from 'react-router-dom'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
import TiptapEditor from '../PagesMedico/TiptapEditor'
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
const EditPageRelatorio = () => {
const params = useParams()
const navigate = useNavigate()
const {getAuthorizationHeader} = useAuth()
let authHeader = getAuthorizationHeader()
const [DictInfo, setDictInfo] = useState({})
let RelatorioID = params.id
const [relatorioData, setRelatorioData] = useState({
patient_id: '',
exam: '',
// Mantemos apenas os campos necessários para o fetch, mas não para edição direta na UI
required_by: '',
address_line: '',
content: '',
})
const [loading, setLoading] = useState(true)
const [patientData, setPatientData] = useState(null) // Armazena dados do paciente
const RelatorioID = params.id
const handleSave = (RelatorioInfos) => {
// Modelo HTML do relatório para ser carregado no Tiptap se o conteúdo for novo/vazio
const generateReportModel = (report, patient) => {
// Escapa as aspas se necessário, mas para HTML simples não é crucial
const patientName = patient?.full_name || 'Paciente não encontrado';
const birthDate = patient?.birth_date || 'Data não informada';
const exam = report?.exam || 'Exame não especificado';
return `
<div>
<p style="text-align: center; font-weight: bold;">Clinica Rise up</p>
<p style="text-align: center;">Dr - CRM/SP 123456</p>
<p style="text-align: center;">Avenida - (79) 9 4444-4444</p>
<br>
<p><strong>Paciente:</strong> ${patientName}</p>
<p><strong>Data de nascimento:</strong> ${birthDate}</p>
<p><strong>Data do exame:</strong> </p>
<p><strong>Exame:</strong> ${exam}</p>
<br>
<p><strong>Conteúdo do Relatório:</strong></p>
<p>1</p>
<br>
<p>Dr</p>
<p>Emitido em: 0</p>
</div>
`;
};
// Função que será chamada ao salvar
const handleSave = () => {
setLoading(true)
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
const raw = JSON.stringify({...RelatorioInfos, order_number:'REL-2025-4386'})
console.log(RelatorioInfos)
// Salva apenas o novo conteúdo do Tiptap (relatorioData.content)
const raw = JSON.stringify({
content: relatorioData.content,
// Você pode manter order_number ou removê-lo se não for editável
order_number: relatorioData.order_number || 'REL-2025-4386'
})
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${RelatorioID}`, requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
.then(result => {
console.log(result);
alert('Relatório atualizado com sucesso!');
setLoading(false)
// MANTIDO: Volta para a área de relatórios
navigate('/medico/relatorios')
})
.catch(error => {
console.log('error', error);
alert('Erro ao atualizar o relatório.');
setLoading(false)
});
}
// Busca os dados do Relatório e do Paciente
useEffect(() => {
const fetchReportData = async () => {
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
let report;
let patient;
try {
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${RelatorioID}`, requestOptions);
const result = await response.json();
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${RelatorioID}`, requestOptions)
.then(response => response.json())
.then(result => setDictInfo(result[0]))
.catch(error => console.log('error', error));
}, [])
if (result.length > 0) {
report = result[0];
console.log(RelatorioID)
// Busca nome do paciente
const patientResult = await GetPatientByID(report.patient_id, authHeader);
if (patientResult.length > 0) {
patient = patientResult[0];
setPatientData(patient);
}
// Determina o conteúdo inicial
let initialContent = report.content || report.diagnosis || report.conclusion || '';
// Se o conteúdo estiver vazio, carrega o modelo do relatório completo
if (!initialContent.trim()) {
initialContent = generateReportModel(report, patient);
}
setRelatorioData({
...report,
content: initialContent,
});
}
} catch (error) {
console.log('error', error);
} finally {
setLoading(false)
}
}
fetchReportData()
}, [RelatorioID, authHeader])
// Função para atualizar o HTML do editor
const handleEditorChange = (newHtml) => {
setRelatorioData(prev => ({ ...prev, content: newHtml }))
}
if (loading) {
return <div>Carregando...</div>
}
return (
<div>
<FormRelatorio DictInfo={DictInfo} setDictInfo={setDictInfo} onSave={handleSave}/>
<div className='container'>
{/* MANTIDO: Título limpo */}
<h3 className='mb-4'>Editar Relatório do Paciente: {patientData?.full_name}</h3>
{/* MUDANÇA: Removidos todos os inputs de texto avulsos */}
{/* Campo do Tiptap Editor */}
<div className='mb-3'>
{/* MUDANÇA: Título ajustado */}
<h5 className='mb-2'>Conteúdo do Relatório</h5>
<TiptapEditor
content={relatorioData.content}
onChange={handleEditorChange}
/>
</div>
<button className='btn btn-success' onClick={handleSave}>
Salvar Relatório
</button>
</div>
)
}

View File

@ -0,0 +1,74 @@
import React from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
// Componente da barra de menu (Menu Bar)
const MenuBar = ({ editor }) => {
if (!editor) {
return null;
}
// Estilos simples para os botões. Você pode e deve estilizar melhor com CSS/Bootstrap.
const buttonStyle = {
marginRight: '4px',
padding: '4px 8px',
cursor: 'pointer',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: editor.isActive('bold') || editor.isActive('italic') ? '#ddd' : 'white',
};
return (
<div style={{ padding: '8px', borderBottom: '1px solid #ccc', display: 'flex', flexWrap: 'wrap' }}>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
style={{ ...buttonStyle, fontWeight: 'bold' }}
>
B
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
style={{ ...buttonStyle, fontStyle: 'italic' }}
>
I
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
style={{ ...buttonStyle, backgroundColor: editor.isActive('bulletList') ? '#ddd' : 'white' }}
>
Lista
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
style={{ ...buttonStyle, backgroundColor: editor.isActive('heading', { level: 2 }) ? '#ddd' : 'white' }}
>
Título 2
</button>
{/* Adicione mais botões conforme a necessidade (link, código, etc.) */}
</div>
);
};
// Componente principal do Editor
const TiptapEditor = ({ content, onChange }) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
// Desativa 'hardBreak' e 'blockquote' se não forem necessários para simplificar
hardBreak: false,
}),
Link, // Adiciona suporte para links
],
content: content || '<p>Inicie o relatório aqui...</p>',
onUpdate: ({ editor }) => {
// Quando o conteúdo muda, chama a função onChange com o HTML
onChange(editor.getHTML());
},
});
return (
<div className='tiptap-editor-container' style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
<MenuBar editor={editor} />
<EditorContent editor={editor} style={{ minHeight: '300px', padding: '10px' }} />
</div>
);
};
export default TiptapEditor;

View File

@ -0,0 +1,15 @@
import React from 'react';
// Componente para renderizar o HTML salvo (conteúdo do editor)
const TiptapViewer = ({ htmlContent }) => {
return (
// 'dangerouslySetInnerHTML' é necessário para renderizar HTML
<div
className="tiptap-viewer"
dangerouslySetInnerHTML={{ __html: htmlContent }}
style={{ border: '1px dashed #eee', padding: '10px' }} // Estilo opcional para destacar o conteúdo
/>
);
};
export default TiptapViewer;

View File

@ -1,24 +1,87 @@
import React from 'react'
import React, { useState, useEffect } from 'react';import { GetDoctorByID } from '../utils/Functions-Endpoints/Doctor';
import { GetPatientByID } from '../utils/Functions-Endpoints/Patient';
import { useAuth } from '../utils/AuthProvider';
import { useNavigate } from 'react-router-dom';
import { useMemo } from 'react';
const CardConsulta = ( {DadosConsulta, TabelaAgendamento} ) => {
const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal} ) => {
const navigate = useNavigate();
const {getAuthorizationHeader} = useAuth()
const authHeader = getAuthorizationHeader()
const [Paciente, setPaciente] = useState()
const [Medico, setMedico] = useState()
const ids = useMemo(() => {
return {
doctor_id: DadosConsulta?.doctor_id,
patient_id: DadosConsulta?.patient_id,
status: DadosConsulta?.status
};
}, [DadosConsulta]);
// Status (agendado, confirmado, realizado, cancelado)
useEffect(() => {
const BuscarMedicoEPaciente = async () => {
if (!ids.doctor_id || !ids.patient_id || ids.status === 'nada') return;
try {
const [Doctor, Patient] = await Promise.all([
GetDoctorByID(ids.doctor_id, authHeader),
GetPatientByID(ids.patient_id, authHeader)
]);
setMedico(Doctor?.[0] || null);
setPaciente(Patient?.[0] || null);
} catch (error) {
console.error('Erro ao buscar médico/paciente:', error);
}
};
BuscarMedicoEPaciente();
}, [ids, authHeader]);
return (
<div className={`container-cardconsulta-${TabelaAgendamento}`}>
{DadosConsulta.status !== 'vazio'?
{DadosConsulta.id?
<div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}>
<div>
<section className='cardconsulta-infosecundaria'>
<p>{DadosConsulta.horario}|GEAP| {DadosConsulta.medico}</p>
<p>{DadosConsulta.horario} {Medico?.full_name}</p>
</section>
<section className='cardconsulta-infoprimaria'>
<p>{DadosConsulta.paciente} - {DadosConsulta.motivo} - 23 anos</p>
<p>{Paciente?.full_name} - {DadosConsulta.exam}</p>
</section>
</div>
<div className='container-botons'>
<button className="btn btn-sm btn-edit-custom"
onClick={() => {navigate(`${DadosConsulta.id}/edit`)}}
>
<i className="bi bi-pencil me-1"></i> Editar
</button>
<button
className="btn btn-sm btn-delete-custom"
onClick={() => {
console.log(DadosConsulta.id)
//setSelectedPatientId(DadosConsulta.id);
setShowDeleteModal(true);
}}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
</div>
</div>
:
null

View File

@ -1,478 +1,134 @@
import InputMask from "react-input-mask";
import "./style/formagendamentos.css";
import { useState, useEffect } from "react";
import { GetPatientByCPF } from "../utils/Functions-Endpoints/Patient";
import { GetDoctorByName } from "../utils/Functions-Endpoints/Doctor";
import { useAuth } from "../utils/AuthProvider";
const FormNovaConsulta = ({ onCancel, patientID }) => {
const [horariosDisponiveis, setHorariosDisponiveis] = useState([]);
const [carregandoHorarios, setCarregandoHorarios] = useState(false);
const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) => {
const {getAuthorizationHeader} = useAuth()
const [isModoEmergencia, setIsModoEmergencia] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [anexos, setAnexos] = useState([]);
const [loadingAnexos, setLoadingAnexos] = useState(false);
const [paciente, setPaciente] = useState({});
const [acessibilidade, setAcessibilidade] = useState({
cadeirante: false,
idoso: false,
gravida: false,
bebe: false,
autista: false,
});
const [dadosAtendimento, setDadosAtendimento] = useState({
profissional: "",
tipoAtendimento: "",
unidade: "",
dataAtendimento: "",
inicio: "",
termino: "",
solicitante: "",
observacoes: "",
});
// Variável de controle para saber se a grade de horário deve ser mostrada
const isReadyForSchedule =
dadosAtendimento.profissional && dadosAtendimento.dataAtendimento;
const fetchHorariosDisponiveis = async (professionalId, date) => {
if (!isReadyForSchedule || isModoEmergencia) {
setHorariosDisponiveis([]);
return;
}
const [acessibilidade, setAcessibilidade] = useState({cadeirante:false,idoso:false,gravida:false,bebe:false, autista:false })
setCarregandoHorarios(true);
setHorariosDisponiveis([]);
let authHeader = getAuthorizationHeader()
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const payload = {
doctor_id: professionalId,
date: date,
};
var requestOptions = {
method: "POST",
headers: myHeaders,
body: JSON.stringify(payload),
redirect: "follow",
};
try {
const res = await fetch(
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability",
requestOptions
);
const data = await res.json();
const slots = data.data && Array.isArray(data.data) ? data.data : [];
setHorariosDisponiveis(slots);
// Limpa o horário se o que estava selecionado não existe mais na nova grade
if (dadosAtendimento.inicio && !slots.includes(dadosAtendimento.inicio)) {
setDadosAtendimento((prev) => ({ ...prev, inicio: "", termino: "" }));
}
} catch (err) {
console.error("Erro ao buscar horários disponíveis:", err);
setHorariosDisponiveis([]);
} finally {
setCarregandoHorarios(false);
}
};
useEffect(() => {
if (!patientID) return;
const fetchAnexos = async () => {
setLoadingAnexos(true);
try {
const res = await fetch(
`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientID}/anexos`
);
const data = await res.json();
setAnexos(data.data || []);
} catch (err) {
console.error("Erro ao buscar anexos:", err);
} finally {
setLoadingAnexos(false);
}
};
fetchAnexos();
}, [patientID]);
useEffect(() => {
// Chama a busca apenas se estivermos no modo padrão E tivermos profissional e data
if (isReadyForSchedule && !isModoEmergencia) {
fetchHorariosDisponiveis(
dadosAtendimento.profissional,
dadosAtendimento.dataAtendimento
);
} else if (!isReadyForSchedule) {
setHorariosDisponiveis([]);
}
}, [
dadosAtendimento.profissional,
dadosAtendimento.dataAtendimento,
isModoEmergencia,
isReadyForSchedule,
]);
const handleUpload = async () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append("file", selectedFile);
try {
const res = await fetch(
`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientID}/anexos`,
{
method: "POST",
body: formData,
}
);
if (res.ok) {
const novoAnexo = await res.json();
setAnexos((prev) => [...prev, novoAnexo]);
setSelectedFile(null);
} else {
console.error("Erro ao enviar anexo");
}
} catch (err) {
console.error("Erro ao enviar anexo:", err);
}
};
const handleclickAcessibilidade = (id) => {
let resultado = acessibilidade[id];
let resultado = acessibilidade[id]
if (resultado === false) {
setAcessibilidade({ ...acessibilidade, [id]: true });
console.log("mudou");
} else if (resultado === true) {
setAcessibilidade({ ...acessibilidade, [id]: false });
if(resultado === false){ setAcessibilidade({...acessibilidade, [id]:true}); console.log('mudou')}
else if(resultado === true){ setAcessibilidade({...acessibilidade, [id]:false})}
console.log(id)
}
console.log(id);
};
const FormatCPF = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
BuscarPacienteExistentePeloCPF(valor);
const digits = String(valor).replace(/\D/g, '').slice(0, 11);
return digits
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
};
const FormatTelefones = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
return digits
.replace(/(\d)/, "($1")
.replace(/(\d{2})(\d)/, "$1) $2")
.replace(/(\d)(\d{4})/, "$1 $2")
.replace(/(\d{4})(\d{4})/, "$1-$2");
};
const BuscarCPFnoBancodeDados = async (cpf) => {
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer <token>");
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({
cpf: cpf,
});
var requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow",
};
const response = await fetch(
"https://mock.apidog.com/m1/1053378-0-default/pacientes/validar-cpf",
requestOptions
);
const result = await response.json();
return result;
};
const BuscarPacienteExistentePeloCPF = async (value) => {
if (isNaN(value[13]) === false && value.length === 14)
try {
const result = await BuscarCPFnoBancodeDados(value);
if (result.data.existe === true) {
var myHeaders = new Headers();
myHeaders.append("Authorization", "Bearer <token>");
var requestOptions = {
method: "GET",
headers: myHeaders,
redirect: "follow",
};
fetch(
"https://mock.apidog.com/m1/1053378-0-default/pacientes/",
requestOptions
)
.then((response) => response.json())
.then((result) => setPaciente(result.data))
.catch((error) => console.log("error", error));
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
}
} catch (error) {
console.log("error", error);
}
};
const handleChange = (e) => {
const { value, name } = e.target;
if (name === "email") {
setPaciente({
...paciente,
contato: {
...paciente.contato,
email: value,
},
});
} else if (name === "telefone") {
setPaciente({
...paciente,
contato: {
...paciente.contato,
telefone1: FormatTelefones(value),
},
});
} else {
setPaciente({ ...paciente, [name]: value });
}
};
const handleAtendimentoChange = (e) => {
const {value, name} = e.target;
setDadosAtendimento((prev) => ({
if(name === 'email'){
setAgendamento({...agendamento, contato:{
...agendamento.contato,
email:value
}})
}else if(name === 'cpf'){
let cpfFormatted = FormatCPF(value)
const fetchPatient = async () => {
let patientData = await GetPatientByCPF(cpfFormatted, authHeader);
if (patientData) {
setAgendamento((prev) => ({
...prev,
[name]: value,
nome: patientData.full_name,
patient_id: patientData.id
}));
};
}}
setAgendamento(prev => ({ ...prev, cpf: cpfFormatted }))
fetchPatient()
}else if(name==='convenio'){
setAgendamento({...agendamento,insurance_provider:value})
}else if(name ==='profissional'){
const handleSubmitExcecao = async () => {
console.log(
"Modo Emergência Ativado: Tentando criar Exceção com novo endpoint."
);
const {
profissional,
dataAtendimento,
tipoAtendimento,
inicio,
termino,
observacoes,
} = dadosAtendimento;
if (
!profissional ||
!dataAtendimento ||
!tipoAtendimento ||
!inicio ||
!termino
) {
alert(
"Por favor, preencha o Profissional, Data, Tipo e Horários para a exceção."
);
return;
const fetchDoctor = async () => {
let DoctorData = await GetDoctorByName(value, authHeader)
if(DoctorData){
setAgendamento((prev) => ({
...prev,
doctor_id:DoctorData.id
}))
}}
fetchDoctor()
}
const payload = {
doctor_id: profissional,
date: dataAtendimento,
start_time: inicio + ":00",
end_time: termino + ":00",
kind: "liberacao",
reason: tipoAtendimento,
};
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var requestOptions = {
method: "POST",
headers: myHeaders,
body: JSON.stringify(payload),
redirect: "follow",
};
try {
const response = await fetch(
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_exceptions",
requestOptions
);
const result = await response.json();
if (response.ok || response.status === 201) {
console.log("Exceção de emergência criada com sucesso:", result);
alert(
`Consulta de emergência agendada como exceção! Detalhes: ${JSON.stringify(
result
)}`
);
} else {
console.error("Erro ao criar exceção de emergência:", result);
alert(
`Erro ao agendar exceção. Status: ${response.status}. Detalhes: ${
result.message || JSON.stringify(result)
}`
);
else{
setAgendamento({...agendamento,[name]:value})
}
} catch (error) {
console.error("Erro na requisição para criar exceção:", error);
alert(
"Erro de comunicação com o servidor ou formato de resposta inválido."
);
}
};
const handleSubmitPadrao = () => {
if (!isReadyForSchedule) {
alert(
"Por favor, preencha o Profissional e a Data do Atendimento antes de salvar."
);
return;
}
if (
!horariosDisponiveis.includes(dadosAtendimento.inicio) ||
!horariosDisponiveis.includes(dadosAtendimento.termino)
) {
alert(
"Por favor, selecione horários válidos dentro da grade do profissional."
);
return;
}
console.log("Salvando agendamento.");
alert("Agendamento salvo!");
};
const handleSubmit = (e) => {
e.preventDefault();
if (isModoEmergencia) {
handleSubmitExcecao();
} else {
handleSubmitPadrao();
}
alert("Agendamento salvo!");
onSave(agendamento)
};
return (
<div className="form-container">
<form className="form-agendamento" onSubmit={handleSubmit}>
<h2 className="section-title">Informações do paciente</h2>
<div
className="campos-informacoes-paciente"
id="informacoes-paciente-linha-um"
>
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-um">
<div className="campo-de-input">
<label>Nome *</label>
<input
type="text"
name="nome"
value={paciente.nome}
placeholder="Insira o nome do paciente"
required
onChange={handleChange}
/>
<input type="text" name="nome" value={agendamento.nome} placeholder="Insira o nome do paciente" required onChange={handleChange} />
</div>
<div className="campo-de-input">
<label>CPF do paciente</label>
<input
type="text"
name="cpf"
placeholder="000.000.000-00"
onChange={(e) => (e.target.value = FormatCPF(e.target.value))}
/>
<input type="text" name="cpf" placeholder="000.000.000-00" onChange={handleChange} value={agendamento.cpf}/>
</div>
<div className="campo-de-input">
<label>RG</label>
<input
type="text"
name="rg"
placeholder="Insira o nº do RG"
maxLength={9}
/>
</div>
</div>
<div
className="campos-informacoes-paciente"
id="informacoes-paciente-linha-dois"
>
<div className="campo-de-input">
<label>Data de nascimento *</label>
<input
type="date"
name="data_nascimento"
value={paciente.data_nascimento}
required
onChange={handleChange}
/>
</div>
<div className="campo-de-input">
<label>Telefone</label>
<input
type="tel"
name="telefone"
placeholder="(99) 99999-9999"
value={paciente.contato?.telefone1}
onChange={handleChange}
/>
</div>
<div className="campo-de-input">
<label>E-mail</label>
<input
type="email"
name="email"
placeholder="Email"
value={paciente.contato?.email}
onChange={handleChange}
/>
</div>
</div>
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
<div
className="campos-informacoes-paciente"
id="informacoes-paciente-linha-tres"
>
<div className="campo-de-input">
<label>Convênio</label>
<select name="convenio">
<option value="particular">Particular</option>
<select name="convenio" onChange={handleChange}>
<option value="publico">Público</option>
<option value="unimed">Unimed</option>
<option value="bradesco_saude">Bradesco Saúde</option>
<option value="hapvida">Hapvida</option>
</select>
</div>
<div className="campo-de-input">
<label>Matrícula</label>
<input type="text" name="matricula" placeholder="000000000" />
</div>
<div className="campo-de-input">
<label>Validade</label>
<input type="date" name="validade" />
</div>
</div>
<h3 className="section-subtitle">Informações adicionais</h3>
<label htmlFor="anexo-input" className="btn btn-secondary">
Adicionar Anexo
</label>
<label htmlFor="anexo-input" className="btn btn-secondary">Adicionar Anexo</label>
<input
type="file"
id="anexo-input"
@ -480,11 +136,7 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
onChange={(e) => setSelectedFile(e.target.files[0])}
/>
{selectedFile && (
<button
type="button"
className="btn btn-primary ms-2"
onClick={handleUpload}
>
<button type="button" className="btn btn-primary ms-2" >
Enviar
</button>
)}
@ -500,280 +152,84 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
)}
</div>
<h2 className="section-title">Informações do atendimento</h2>
<div className="emergencia-toggle-container">
<button
type="button"
className={`btn ${
isModoEmergencia ? "btn-danger" : "btn-secondary"
}`}
onClick={() => setIsModoEmergencia((prev) => !prev)}
style={{ marginBottom: "15px" }}
>
{isModoEmergencia
? "Modo: EMERGÊNCIA Ativo"
: "Ativar Modo: Emergência (Exceção)"}
</button>
{isModoEmergencia && (
<p className="alerta-emergencia">
As informações de data e horário serão enviadas como uma
exceção fora da grade normal.
</p>
)}
</div>
<div className="icons-container">
<div
className={`icons-div ${
acessibilidade.cadeirante === true ? "acessibilidade-ativado" : ""
} `}
id="cadeirante"
onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}
>
<div className={`icons-div ${ acessibilidade.cadeirante === true ? 'acessibilidade-ativado' : ''} `} id='cadeirante' onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}>
<span className="material-symbols-outlined icon">accessible</span>
</div>
<div
className={`icons-div ${
acessibilidade.idoso === true ? "acessibilidade-ativado" : ""
}`}
id="idoso"
onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}
>
<div className={`icons-div ${acessibilidade.idoso === true ? 'acessibilidade-ativado' : ''}`} id="idoso" onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}>
<span className="material-symbols-outlined icon">elderly</span>
</div>
<div
className={`icons-div ${
acessibilidade.gravida === true ? "acessibilidade-ativado" : ""
}`}
id="gravida"
onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}
>
<span className="material-symbols-outlined icon">
pregnant_woman
</span>
<div className={`icons-div ${acessibilidade.gravida === true ? 'acessibilidade-ativado' : ''}`} id="gravida" onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}>
<span className="material-symbols-outlined icon">pregnant_woman</span>
</div>
<div
className={`icons-div ${
acessibilidade.bebe === true ? "acessibilidade-ativado" : ""
}`}
id="bebe"
onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="34"
height="34"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-baby-icon lucide-baby"
>
<path d="M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5" />
<path d="M15 12h.01" />
<path d="M19.38 6.813A9 9 0 0 1 20.8 10.2a2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1" />
<path d="M9 12h.01" />
</svg>
<div className={`icons-div ${acessibilidade.bebe === true ? 'acessibilidade-ativado' : ''}`} id="bebe" onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}>
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-baby-icon lucide-baby"><path d="M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5"/><path d="M15 12h.01"/><path d="M19.38 6.813A9 9 0 0 1 20.8 10.2a2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1"/><path d="M9 12h.01"/></svg>
</div>
<div
className={`icons-div ${
acessibilidade.autista === true ? "acessibilidade-ativado" : ""
}`}
id="autista"
onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.75"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-puzzle-icon lucide-puzzle"
>
<path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z" />
</svg>
<div className={`icons-div ${acessibilidade.autista === true ? 'acessibilidade-ativado' : ''}`} id="autista" onClick={(e) => handleclickAcessibilidade(e.currentTarget.id)}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-puzzle-icon lucide-puzzle"><path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"/></svg>
</div>
</div>
<div className="campo-informacoes-atendimento">
<div className="campo-de-input">
<label>Nome do profissional *</label>
{/* INPUT: Nome do profissional (usado para habilitar a busca de horários) */}
<input
type="text"
name="profissional"
required
value={dadosAtendimento.profissional}
onChange={handleAtendimentoChange}
/>
<input type="text" name="profissional" onChange={handleChange} value={agendamento.nome_medico}required />
</div>
<div className="campo-de-input">
<label>Tipo de atendimento *</label>
<input
type="text"
name="tipoAtendimento"
required
value={dadosAtendimento.tipoAtendimento}
onChange={handleAtendimentoChange}
/>
<input type="text" name="tipoAtendimento" required />
</div>
</div>
<section id="informacoes-atendimento-segunda-linha">
<section id="informacoes-atendimento-segunda-linha-esquerda">
<div className="campo-informacoes-atendimento">
<div className="campo-de-input">
<div className='campo-de-input'>
<label>Unidade *</label>
<select
name="unidade"
value={dadosAtendimento.unidade}
onChange={handleAtendimentoChange}
>
<option value="" disabled invisible selected>
Selecione a unidade
</option>
<option value="centro">
Núcleo de Especialidades Integradas
</option>
<select name="unidade">
<option value="" disabled invisible selected>Selecione a unidade</option>
<option value="centro">Núcleo de Especialidades Integradas</option>
<option value="leste">Unidade Leste</option>
</select>
</div>
<div className="campo-de-input">
<label>Data *</label>
{/* INPUT: Data de Atendimento (usada para habilitar a busca de horários) */}
<input
type="date"
name="dataAtendimento"
required
value={dadosAtendimento.dataAtendimento}
onChange={handleAtendimentoChange}
/>
<input type="date" name="dataAtendimento" required />
</div>
</div>
<div className="campo-informacoes-atendimento">
{isModoEmergencia ? (
// MODO EMERGÊNCIA: Input type="time" simples, sem restrição
<>
<div className="campo-de-input">
<label>Início *</label>
<input
type="time"
name="inicio"
required
value={dadosAtendimento.inicio}
onChange={handleAtendimentoChange}
/>
</div>
<div className="campo-de-input">
<label>Término *</label>
<input
type="time"
name="termino"
required
value={dadosAtendimento.termino}
onChange={handleAtendimentoChange}
/>
</div>
</>
) : // MODO PADRÃO
isReadyForSchedule ? (
// ESTADO 2: Médico e Data ESCOLHIDOS -> Restringe para grade (SELECT)
<>
<div className="campo-de-input">
<label>Início *</label>
{carregandoHorarios ? (
<select disabled>
<option>Carregando horários...</option>
</select>
) : (
<select
name="inicio"
required
value={dadosAtendimento.inicio}
onChange={handleAtendimentoChange}
disabled={horariosDisponiveis.length === 0}
>
<option value="" disabled>
{horariosDisponiveis.length === 0
? "Nenhum horário disponível"
: "Selecione o horário de início"}
</option>
{horariosDisponiveis.map((slot) => (
<option key={slot} value={slot}>
{slot}
</option>
))}
</select>
)}
<input type="time" name="inicio" required />
</div>
<div className="campo-de-input">
<label>Término *</label>
<select
name="termino"
required
value={dadosAtendimento.termino}
onChange={handleAtendimentoChange}
disabled={horariosDisponiveis.length === 0}
>
<option value="" disabled>
Selecione o horário de término
</option>
{horariosDisponiveis.map((slot) => (
<option key={slot} value={slot}>
{slot}
</option>
))}
</select>
<input type="time" name="termino" required />
</div>
</>
) : (
// ESTADO 1: Médico ou Data PENDENTE -> Permite entrada de tempo livre (INPUT TYPE="TIME")
<>
<div className="campo-de-input">
<label>Início *</label>
<input
type="time"
name="inicio"
required
value={dadosAtendimento.inicio}
onChange={handleAtendimentoChange}
/>
</div>
<div className="campo-de-input">
<label>Término *</label>
<input
type="time"
name="termino"
required
value={dadosAtendimento.termino}
onChange={handleAtendimentoChange}
/>
</div>
</>
)}
<div className="campo-de-input">
<label>Profissional solicitante</label>
<select
name="solicitante"
value={dadosAtendimento.solicitante}
onChange={handleAtendimentoChange}
>
<option value="" disabled invisible selected>
Selecione o solicitante
</option>
<select name="solicitante">
<option value="" disabled invisible selected>Selecione o solicitante</option>
<option value="secretaria">Secretária</option>
<option value="medico">Médico</option>
</select>
@ -782,28 +238,21 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
</section>
<section className="informacoes-atendimento-segunda-linha-direita">
<div className="campo-de-input">
<label>Observações</label>
<textarea
name="observacoes"
rows="4"
cols="1"
value={dadosAtendimento.observacoes}
onChange={handleAtendimentoChange}
></textarea>
<textarea name="observacoes" rows="4" cols="1"></textarea>
</div>
</section>
</section>
<div className="form-actions">
<button type="submit" className="btn-primary">
Salvar agendamento
</button>
<button type="button" className="btn-cancel" onClick={onCancel}>
Cancelar
</button>
<button type="submit" className="btn-primary">Salvar agendamento</button>
<button type="button" className="btn-cancel" onClick={onCancel}>Cancelar</button>
</div>
</form>
</div>
);
};

View File

@ -1,14 +1,34 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabeladia.css";
const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos }) => {
const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDeleteModal }) => {
const [indiceAcesso, setIndiceAcesso] = useState(0)
const [Dia, setDia] = useState()
const agendamentosDoDia = agendamentos?.semana1?.segunda || [];
const nomeMedico = agendamentosDoDia.find(item => item.medico)?.medico || 'Profissional';
let ListaDiasComAgendamentos = Object.keys(agendamentos)
console.log(Dia, "hshdhshhsdhs")
useEffect(() => {
setDia(ListaDiasComAgendamentos[indiceAcesso])
}, [indiceAcesso])
return (
<div>
<div>
<div id='tabela-seletor-container'>
<button onClick={() => {if(indiceAcesso === 0)return; else(setIndiceAcesso(indiceAcesso - 1))}}> <i className="bi bi-chevron-compact-left"></i></button>
<p>{Dia}</p>
<button onClick={() => {if(ListaDiasComAgendamentos.length - 1 === indiceAcesso)return; else(setIndiceAcesso(indiceAcesso + 1))}}> <i className="bi bi-chevron-compact-right"></i></button>
</div>
</div>
<table className='tabeladiaria'>
<thead>
<tr>
@ -18,12 +38,12 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos }) => {
</thead>
<tbody>
{agendamentosDoDia.map((agendamento, index) => (
{agendamentos[Dia]?.map((agendamento, index) => (
<tr key={index}>
<td><p>{agendamento.horario}</p></td>
<td className='mostrar-horario'>
<div onClick={() => handleClickAgendamento(agendamento)}>
<CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} />
<CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} setShowDeleteModal={setShowDeleteModal} />
</div>
</td>
</tr>

View File

@ -3,7 +3,8 @@ import React from 'react';
import dayjs from "dayjs";
import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabelames.css";
import { useEffect, useState } from 'react';
import { useMemo } from 'react';
const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
@ -12,19 +13,179 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
const mes = dataHoje.month() + 1;
let ListaDiasDatas = ListarDiasdoMes(AnoAtual, mes);
const [AgendamentosSemanaisOrganizados, setAgendamentosSemanaisOrganizados] = useState({})
const [indice, setIndice] = useState("10")
const [AgendamentosMensaisOrganizados, setAgendamentosMensaisOrganizados] = useState({
"01": { "nomeDoMes": "janeiro" },
"02": { "nomeDoMes": "fevereiro" },
"03": { "nomeDoMes": "março" },
"04": { "nomeDoMes": "abril" },
"05": { "nomeDoMes": "maio" },
"06": { "nomeDoMes": "junho" },
"07": { "nomeDoMes": "julho" },
"08": { "nomeDoMes": "agosto" },
"09": { "nomeDoMes": "setembro" },
"10": { "nomeDoMes": "outubro" },
"11": { "nomeDoMes": "novembro" },
"12": { "nomeDoMes": "dezembro" }
})
const OrganizarAgendamentosSemanais = useMemo(() => {
if (!agendamentos || Object.keys(agendamentos).length === 0) return {};
const DiasComAtendimentos = Object.keys(agendamentos)
const semanas = {}
for (let i = 0; i < DiasComAtendimentos.length; i++) {
const DiaComAtendimento = DiasComAtendimentos[i]
const [_, MesDoAgendamento, DiaDoAgendamento] = DiaComAtendimento.split("-")
const data = dayjs(`${AnoAtual}-${MesDoAgendamento}-${DiaDoAgendamento}`)
const diaSemana = data.format('dddd')
const semanaKey = `semana${data.week()}`
if (!semanas[semanaKey]) {
semanas[semanaKey] = {
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
}
}
switch (diaSemana) {
case 'Monday':
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break
case 'Tuesday':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break
case 'Wednesday':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break
case 'Thursday':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break
case 'Friday':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break
default:
break
}
}
return semanas
}, [agendamentos, AnoAtual])
useEffect(() => {
setAgendamentosSemanaisOrganizados(OrganizarAgendamentosSemanais);
// NOTA: Ao carregar, o Indice é 0, que é a primeira semana.
}, [OrganizarAgendamentosSemanais])
useEffect(() => {
console.log(OrganizarAgendamentosMensais)
}, [])
useEffect(() => {
console.log(AgendamentosMensaisOrganizados, 'aqui os agendamentos mensais')
}, [AgendamentosMensaisOrganizados])
const OrganizarAgendamentosMensais = useMemo(() => {
if (!AgendamentosSemanaisOrganizados || Object.keys(AgendamentosSemanaisOrganizados).length === 0)
return;
// Cria uma cópia local do estado atual
const novoEstado = { ...AgendamentosMensaisOrganizados };
const indices = Object.keys(AgendamentosSemanaisOrganizados);
for (let i = 0; i < indices.length; i++) {
const DictSemanais = AgendamentosSemanaisOrganizados[indices[i]];
const indicesDictSemanais = Object.keys(DictSemanais);
for (let d = 0; d < indicesDictSemanais.length; d++) {
const lista = DictSemanais[indicesDictSemanais[d]];
if (lista.length > 0) {
const [_, mesDaConsulta] = lista[0].scheduled_at.split("-");
// Cria o mês se ainda não existir
if (!novoEstado[mesDaConsulta]) {
novoEstado[mesDaConsulta] = {
nomeDoMes: AgendamentosMensaisOrganizados[mesDaConsulta]?.nomeDoMes || "",
};
}
// Garante que a semana existe
novoEstado[mesDaConsulta][indices[i]] = {
...novoEstado[mesDaConsulta][indices[i]],
...DictSemanais,
};
}
}
}
// Faz o set de uma vez só
setAgendamentosMensaisOrganizados(novoEstado);
}, [AgendamentosSemanaisOrganizados]);
const AvançarMes = () => {
let Indice = parseInt(indice)
Indice += 1
console.log(Indice)
if(Indice < 10){
Indice = "0" + Indice.toString()
console.log(Indice)
}
if(Indice === 13){
return
}else{
setIndice(Indice)
}
}
const VoltarMes = () => {
let Indice = parseInt(indice)
Indice -= 1
console.log(Indice)
if(Indice < 10){
Indice = "0" + Indice.toString()
console.log(Indice)
}
if(Indice === "00"){
return
}else{
setIndice(Indice)
}
}
let segundas = ListaDiasDatas[0];
let tercas = ListaDiasDatas[1];
let quartas = ListaDiasDatas[2];
let quintas = ListaDiasDatas[3];
let sextas = ListaDiasDatas[4];
return (
<div>
<div >
<div id='tabela-seletor-container'>
<button onClick={() => VoltarMes()}><i className="bi bi-chevron-compact-left"></i></button>
<p>{AgendamentosMensaisOrganizados[indice].nomeDoMes}</p>
<button onClick={() => AvançarMes()}> <i className="bi bi-chevron-compact-right"></i> </button>
</div>
</div>
<table className='tabelamensal'>
<thead>
<tr>
<th>Seg</th>
<td className='cabecalho-tabela'>Seg</td>
<th>Ter</th>
<th>Qua</th>
<th>Qui</th>
@ -32,104 +193,32 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
</tr>
</thead>
<tbody>
{agendamentos && Object.entries(agendamentos).map(([semana, dias], index) => (
<tr key={index}>
{/* Coluna de Segunda-feira */}
<td>
<div>
<p>{segundas[index]}</p>
<div>
{(dias.segunda || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
</div>
{(dias.segunda || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.segunda || []).length - 3}</p></div>
: null}
</div>
</td>
{Object.keys(AgendamentosMensaisOrganizados[indice]).map((semanaKey) => {
const semana = AgendamentosMensaisOrganizados[indice][semanaKey]
console.log(AgendamentosMensaisOrganizados[indice][semanaKey], 'ajdsahchbaohdfoduh')
{/* Coluna de Terça-feira */}
<td>
<div>
<p>{tercas[index]}</p>
<div>
{(dias.terca || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
</div>
{(dias.terca || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.terca || []).length - 3}</p></div>
: null}
</div>
</td>
{/* Coluna de Quarta-feira */}
<td>
<div>
<p>{quartas[index]}</p>
<div>
{(dias.quarta || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
</div>
{(dias.quarta || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.quarta || []).length - 3}</p></div>
: null}
</div>
</td>
return(
<tr key={semanaKey}>
{/* Coluna de Quinta-feira */}
<td>
{
semana && typeof semana === "object" && Object.keys(semana).map((dia) => (
<td key={dia} >
<CardConsulta DadosConsulta={((semana[dia]|| [])[0]) || {status:'vazio'}}/>
<CardConsulta DadosConsulta={((semana[dia]|| [])[1]) || {status:'vazio'}}/>
<CardConsulta DadosConsulta={((semana[dia]|| [])[2]) || {status:'vazio'}}/>
{semana[dia].length > 3 ? (
<div>
<p>{quintas[index]}</p>
<div>
{(dias.quinta || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
<p>{` +${semana[dia].length - 2}`}</p>
</div>
{(dias.quinta || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.quinta || []).length - 3}</p></div>
: null}
</div>
</td>
{/* Coluna de Sexta-feira */}
<td>
<div>
<p>{sextas[index]}</p>
<div>
{(dias.sexta || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
</div>
{(dias.sexta || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.sexta || []).length - 3}</p></div>
: null}
</div>
): null }
</td>
))
}
</tr>
))}
)})}
</tbody>
</table>
</div>

View File

@ -1,31 +1,150 @@
import React from 'react';
import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabelasemana.css";
import dayjs from 'dayjs';
import { useEffect, useState, useMemo } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear'
dayjs.extend(weekOfYear)
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes }) => {
// Armazena o objeto COMPLETO das semanas organizadas
const [semanasOrganizadas, setSemanasOrganizadas] = useState({});
// Controla qual semana está sendo exibida (o índice da chave no objeto)
const [Indice, setIndice] = useState(0);
const dataHoje = dayjs();
const AnoAtual = dataHoje.year();
const mes = dataHoje.month() + 1;
let DiasdoMes = ListarDiasdoMes(AnoAtual, mes)
// Array de chaves (ex: ['semana40', 'semana41', ...])
const chavesDasSemanas = Object.keys(semanasOrganizadas);
// Armazena o total de semanas que foram organizadas (para definir os limites de navegação)
const totalSemanas = chavesDasSemanas.length;
// --- LÓGICA DE ORGANIZAÇÃO (useMemo mantido para otimização) ---
const OrganizarAgendamentosSemanais = useMemo(() => {
if (!agendamentos || Object.keys(agendamentos).length === 0) return {};
const DiasComAtendimentos = Object.keys(agendamentos)
const semanas = {}
for (let i = 0; i < DiasComAtendimentos.length; i++) {
const DiaComAtendimento = DiasComAtendimentos[i]
const [_, MesDoAgendamento, DiaDoAgendamento] = DiaComAtendimento.split("-")
const data = dayjs(`${AnoAtual}-${MesDoAgendamento}-${DiaDoAgendamento}`)
const diaSemana = data.format('dddd')
const semanaKey = `semana${data.week()}`
if (!semanas[semanaKey]) {
semanas[semanaKey] = {
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
}
}
switch (diaSemana) {
case 'Monday':
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break
case 'Tuesday':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break
case 'Wednesday':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break
case 'Thursday':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break
case 'Friday':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break
default:
break
}
}
return semanas
}, [agendamentos, AnoAtual]) // Adicionei AnoAtual como dependência por segurança
// --- EFEITO PARA POPULAR O ESTADO ---
useEffect(() => {
setSemanasOrganizadas(OrganizarAgendamentosSemanais);
// NOTA: Ao carregar, o Indice é 0, que é a primeira semana.
}, [OrganizarAgendamentosSemanais])
// --- NOVAS FUNÇÕES DE NAVEGAÇÃO ---
const avancarSemana = () => {
// Avança se o índice atual não for o último (totalSemanas - 1)
if (Indice < totalSemanas - 1) {
setIndice(Indice + 1);
}
};
const voltarSemana = () => {
// Volta se o índice atual não for o primeiro (0)
if (Indice > 0) {
setIndice(Indice - 1);
}
};
const TabelaAgendamentoSemana = ({ agendamentos }) => {
// --- PREPARAÇÃO DOS DADOS PARA RENDERIZAÇÃO ---
// Pega a chave da semana que deve ser exibida (usa o estado Indice)
const chaveDaSemanaAtual = chavesDasSemanas[Indice];
const agendamentoSemana = agendamentos?.semana1 || {};
const agendamentosDeSegunda = agendamentoSemana.segunda || [];
const agendamentosDeTerca = agendamentoSemana.terca || [];
const agendamentosDeQuarta = agendamentoSemana.quarta || [];
const agendamentosDeQuinta = agendamentoSemana.quinta || [];
const agendamentosDeSexta = agendamentoSemana.sexta || [];
// Extrai os agendamentos da semana atual (ou um objeto vazio se não existir)
const semanaParaRenderizar = semanasOrganizadas[chaveDaSemanaAtual] || {
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
};
// Determina o número máximo de linhas/consultas
const numLinhas = Math.max(
agendamentosDeSegunda.length,
agendamentosDeTerca.length,
agendamentosDeQuarta.length,
agendamentosDeQuinta.length,
agendamentosDeSexta.length
semanaParaRenderizar.segunda.length,
semanaParaRenderizar.terça.length,
semanaParaRenderizar.quarta.length,
semanaParaRenderizar.quinta.length,
semanaParaRenderizar.sexta.length
);
// Array de índices para iterar sobre as LINHAS da tabela
const indicesDeLinha = Array.from({ length: numLinhas }, (_, i) => i);
// Título da semana (para mostrar ao usuário)
const tituloSemana = chaveDaSemanaAtual
? `Semana ${chaveDaSemanaAtual.replace('semana', '')} / ${AnoAtual}`
: 'Nenhuma semana encontrada';
// --- RENDERIZAÇÃO ---
return (
<div>
{/* Container de Navegação */}
<div id='tabela-seletor-container'>
<button
onClick={voltarSemana}
disabled={Indice === 0} // Desabilita se for a primeira semana
>
<i className='bi bi-chevron-compact-left'></i>
</button>
<p>{tituloSemana}</p>
<button
onClick={avancarSemana}
disabled={Indice === totalSemanas - 1 || totalSemanas === 0} // Desabilita se for a última semana ou se não houver semanas
>
<i className='bi bi-chevron-compact-right'></i>
</button>
</div>
{/* Tabela de Agendamentos */}
<table className='tabelasemanal'>
<thead>
<tr>
@ -38,28 +157,44 @@ const TabelaAgendamentoSemana = ({ agendamentos }) => {
</tr>
</thead>
<tbody>
{Array.from({ length: numLinhas }).map((_, index) => {
{indicesDeLinha.map((indiceLinha) => (
<tr key={indiceLinha}>
{/* Célula para Horário (Pode ser ajustado para mostrar o horário real) */}
<td></td>
const consultaSeg = agendamentosDeSegunda[index];
const consultaTer = agendamentosDeTerca[index];
const consultaQua = agendamentosDeQuarta[index];
const consultaQui = agendamentosDeQuinta[index];
const consultaSex = agendamentosDeSexta[index];
const horarioDaLinha = consultaSeg?.horario || consultaTer?.horario || consultaQua?.horario || consultaQui?.horario || consultaSex?.horario;
return (
<tr key={index}>
<td>{horarioDaLinha}</td>
<td>{consultaSeg && <CardConsulta DadosConsulta={consultaSeg} />}</td>
<td>{consultaTer && <CardConsulta DadosConsulta={consultaTer} />}</td>
<td>{consultaQua && <CardConsulta DadosConsulta={consultaQua} />}</td>
<td>{consultaQui && <CardConsulta DadosConsulta={consultaQui} />}</td>
<td>{consultaSex && <CardConsulta DadosConsulta={consultaSex} />}</td>
{/* Mapeamento de COLUNAS (dias) */}
<td>
{semanaParaRenderizar.segunda[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} />
: null
}
</td>
<td>
{semanaParaRenderizar.terça[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} />
: null
}
</td>
<td>
{semanaParaRenderizar.quarta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} />
: null
}
</td>
<td>
{semanaParaRenderizar.quinta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} />
: null
}
</td>
<td>
{semanaParaRenderizar.sexta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} />
: null
}
</td>
</tr>
);
})}
))}
</tbody>
</table>
</div>

View File

@ -115,3 +115,8 @@ html[data-bs-theme="dark"] .mostrar-horario th {
color: #e0e0e0;
background-color: #232323;
}
/*
.container-botons{
margin-left: 10rem;
background-color: pink;
}*/

View File

@ -220,3 +220,13 @@ html[data-bs-theme="dark"] .usuario-default {
html[data-bs-theme="dark"] .cards-que-faltam {
color: #90caf9;
}
.cabecalho-tabela{
color: white;
background-color: #005a9e;
}
.container-botons{
margin-left: 5rem;
}

View File

@ -0,0 +1,107 @@
/* src/components/Header/Header.css */
.header-container {
width: 100%;
position: absolute; /* Permite posicionamento livre sobre o conteúdo */
top: 0;
left: 0;
display: flex;
justify-content: flex-end; /* Alinha os elementos do container à direita */
padding: 10px 20px;
box-sizing: border-box;
z-index: 1000; /* Garante que fique acima de outros elementos */
}
.right-corner-elements {
display: flex;
align-items: center;
gap: 20px; /* Espaço entre o telefone e a seção de perfil */
}
/* --- ÍCONE DE TELEFONE --- */
.phone-icon-container {
font-size: 24px;
cursor: pointer;
padding: 5px; /* Área clicável um pouco maior */
}
.phone-icon {
display: block; /* Garante que o emoji fique bem centralizado */
}
/* --- SEÇÃO DE PERFIL --- */
.profile-section {
position: relative;
display: flex;
align-items: center;
}
.profile-picture-container {
width: 40px;
height: 40px;
border-radius: 50%; /* Círculo */
overflow: hidden;
cursor: pointer;
border: 2px solid #ccc; /* Borda simples */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
.profile-placeholder {
width: 100%;
height: 100%;
background-color: #A9A9A9; /* Cor cinza escura para o fundo */
border-radius: 50%;
/* Adicionando um ícone simples de pessoa em branco para simular o ícone */
position: relative;
}
.profile-placeholder::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="%23FFFFFF" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3c0 16.7 13.5 30.2 30.2 30.2h387.6c16.7 0 30.2-13.5 30.2-30.2 0-98.5-79.8-178.3-178.3-178.3h-45.7z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
opacity: 0.8; /* Suaviza um pouco o ícone */
}
/* --- DROPDOWN (MENU) --- */
.profile-dropdown {
position: absolute;
top: 50px; /* Posição abaixo da foto de perfil (40px + 10px de espaço) */
right: 0;
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
z-index: 10;
min-width: 150px;
overflow: hidden; /* Garante que bordas fiquem visíveis */
}
.dropdown-button {
background: none;
border: none;
padding: 10px 15px;
text-align: left;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.2s;
}
.dropdown-button:hover {
background-color: #f0f0f0;
}
.logout-button {
color: #cc0000; /* Cor vermelha para o botão de logout */
}
.logout-button:hover {
background-color: #ffe0e0;
}

View File

@ -0,0 +1,60 @@
// src/components/Header/Header.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './Header.css';
const Header = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const navigate = useNavigate();
const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen);
};
const handleViewProfile = () => {
// Redireciona para uma página de perfil (Rota que adicionaremos no App.js)
navigate('/perfil');
setIsDropdownOpen(false);
};
const handleLogout = () => {
// Ação de Logout: Exibe um alerta e redireciona para a tela de Login
alert('Você foi desconectado. Executando ação de logout...');
setIsDropdownOpen(false);
navigate('/login');
};
const handleSupportClick = () => {
// Funcionalidade de suporte (futuramente implementada em TelefoneSuporte)
alert('Função de Suporte de Telefone em desenvolvimento.');
};
return (
<div className="header-container">
<div className="right-corner-elements">
{/* Ícone de Telefone */}
<div className="phone-icon-container" onClick={handleSupportClick}>
<span className="phone-icon" role="img" aria-label="telefone">📞</span>
</div>
{/* Seção de Perfil com Dropdown */}
<div className="profile-section">
<div className="profile-picture-container" onClick={handleProfileClick}>
{/* O div "profile-placeholder" simula a foto de perfil circular colorida */}
<div className="profile-placeholder"></div>
</div>
{isDropdownOpen && (
<div className="profile-dropdown">
<button onClick={handleViewProfile} className="dropdown-button">Ver Perfil</button>
<button onClick={handleLogout} className="dropdown-button logout-button">Sair (Logout)</button>
</div>
)}
</div>
</div>
</div>
);
};
export default Header;

View File

@ -33,6 +33,7 @@ const TrocardePerfis = () => {
{ key: "medico", label: "Médico", route: "/medico" },
{ key: "financeiro", label: "Financeiro", route: "/financeiro" },
{ key: "admin", label: "Administração", route: "/admin" },
{ key: "paciente", label: "Paciente", route: "/paciente" },
].filter(
(opt) =>
showProfiles?.includes(opt.key) || showProfiles?.includes("admin")

View File

@ -1,35 +1,37 @@
import React, { useState, useRef } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import React, { useState, useRef } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const navigate = useNavigate();
const location = useLocation();
const FormatTelefones = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
const digits = String(valor).replace(/\D/g, '').slice(0, 11);
return digits
.replace(/(\d)/, "($1")
.replace(/(\d{2})(\d)/, "$1) $2")
.replace(/(\d)(\d{4})/, "$1 $2")
.replace(/(\d{4})(\d{4})/, "$1-$2");
.replace(/(\d)/, '($1')
.replace(/(\d{2})(\d)/, '$1) $2')
.replace(/(\d)(\d{4})/, '$1 $2')
.replace(/(\d{4})(\d{4})/, '$1-$2');
};
const FormatCPF = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
const digits = String(valor).replace(/\D/g, '').slice(0, 11);
return digits
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
};
const validarCPF = (cpf) => {
const cpfLimpo = cpf.replace(/\D/g, "");
const cpfLimpo = cpf.replace(/\D/g, '');
if (cpfLimpo.length !== 11) return false;
if (/^(\d)\1+$/.test(cpfLimpo)) return false;
let soma = 0;
for (let i = 0; i < 9; i++) {
soma += parseInt(cpfLimpo.charAt(i)) * (10 - i);
@ -45,16 +47,14 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
resto = 11 - (soma % 11);
let digito2 = resto === 10 || resto === 11 ? 0 : resto;
return (
digito1 === parseInt(cpfLimpo.charAt(9)) &&
digito2 === parseInt(cpfLimpo.charAt(10))
);
return digito1 === parseInt(cpfLimpo.charAt(9)) && digito2 === parseInt(cpfLimpo.charAt(10));
};
const [avatarUrl, setAvatarUrl] = useState(null);
const [showRequiredModal, setShowRequiredModal] = useState(false);
const [emptyFields, setEmptyFields] = useState([]);
const [cpfError, setCpfError] = useState("");
const [cpfError, setCpfError] = useState('');
const nomeRef = useRef(null);
const cpfRef = useRef(null);
@ -70,59 +70,63 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
});
const handleToggleCollapse = (section) => {
setCollapsedSections((prevState) => ({
setCollapsedSections(prevState => ({
...prevState,
[section]: !prevState[section],
[section]: !prevState[section]
}));
};
const handleChange = (e) => {
const { name, value, type, checked, files } = e.target;
if (value && emptyFields.includes(name)) {
setEmptyFields((prev) => prev.filter((field) => field !== name));
setEmptyFields(prev => prev.filter(field => field !== name));
}
if (name === "cpf" && cpfError) {
setCpfError("");
if (name === 'cpf' && cpfError) {
setCpfError('');
}
if (type === "checkbox") {
setFormData((prev) => ({ ...prev, [name]: checked }));
} else if (type === "file") {
setFormData((prev) => ({ ...prev, [name]: files[0] }));
if (type === 'checkbox') {
setFormData(prev => ({ ...prev, [name]: checked }));
} else if (type === 'file') {
setFormData(prev => ({ ...prev, [name]: files[0] }));
if (name === "foto" && files[0]) {
if (name === 'foto' && files[0]) {
const reader = new FileReader();
reader.onloadend = () => {
setAvatarUrl(reader.result);
};
reader.readAsDataURL(files[0]);
} else if (name === "foto" && !files[0]) {
} else if (name === 'foto' && !files[0]) {
setAvatarUrl(null);
}
} else if (name.includes("cpf")) {
let cpfFormatado = FormatCPF(value);
setFormData((prev) => ({ ...prev, [name]: cpfFormatado }));
const cpfLimpo = cpfFormatado.replace(/\D/g, "");
} else if (name.includes('cpf')) {
let cpfFormatado = FormatCPF(value);
setFormData(prev => ({ ...prev, [name]: cpfFormatado }));
const cpfLimpo = cpfFormatado.replace(/\D/g, '');
if (cpfLimpo.length === 11) {
if (!validarCPF(cpfFormatado)) {
setCpfError("CPF inválido");
setCpfError('CPF inválido');
} else {
setCpfError("");
setCpfError('');
}
}
} else if (name.includes("phone")) {
} else if (name.includes('phone')) {
let telefoneFormatado = FormatTelefones(value);
setFormData((prev) => ({ ...prev, [name]: telefoneFormatado }));
setFormData(prev => ({ ...prev, [name]: telefoneFormatado }));
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, "");
const cep = formData.cep?.replace(/\D/g, '');
if (cep && cep.length === 8) {
try {
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
@ -130,49 +134,50 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
if (!data.erro) {
setFormData((prev) => ({
...prev,
street: data.logradouro || "",
neighborhood: data.bairro || "",
city: data.localidade || "",
state: data.uf || "",
street: data.logradouro || '',
neighborhood: data.bairro || '',
city: data.localidade || '',
state: data.uf || ''
}));
} else {
setShowRequiredModal(true);
setEmptyFields(["cep"]);
setEmptyFields(['cep']);
}
} catch (error) {
setShowRequiredModal(true);
setEmptyFields(["cep"]);
setEmptyFields(['cep']);
}
}
};
const scrollToEmptyField = (fieldName) => {
let fieldRef = null;
switch (fieldName) {
case "full_name":
case 'full_name':
fieldRef = nomeRef;
setCollapsedSections((prev) => ({ ...prev, dadosPessoais: true }));
setCollapsedSections(prev => ({ ...prev, dadosPessoais: true }));
break;
case "cpf":
case 'cpf':
fieldRef = cpfRef;
setCollapsedSections((prev) => ({ ...prev, dadosPessoais: true }));
setCollapsedSections(prev => ({ ...prev, dadosPessoais: true }));
break;
case "email":
case 'email':
fieldRef = emailRef;
setCollapsedSections((prev) => ({ ...prev, contato: true }));
setCollapsedSections(prev => ({ ...prev, contato: true }));
break;
case "phone_mobile":
case 'phone_mobile':
fieldRef = telefoneRef;
setCollapsedSections((prev) => ({ ...prev, contato: true }));
setCollapsedSections(prev => ({ ...prev, contato: true }));
break;
case "crm_uf":
case 'crm_uf':
fieldRef = crmUfRef;
setCollapsedSections((prev) => ({ ...prev, dadosPessoais: true }));
setCollapsedSections(prev => ({ ...prev, dadosPessoais: true }));
break;
case "crm":
case 'crm':
fieldRef = crmRef;
setCollapsedSections((prev) => ({ ...prev, dadosPessoais: true }));
setCollapsedSections(prev => ({ ...prev, dadosPessoais: true }));
break;
default:
return;
@ -182,19 +187,20 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
setTimeout(() => {
if (fieldRef.current) {
fieldRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
behavior: 'smooth',
block: 'center'
});
fieldRef.current.focus();
fieldRef.current.style.border = "2px solid #dc3545";
fieldRef.current.style.boxShadow =
"0 0 0 0.2rem rgba(220, 53, 69, 0.25)";
fieldRef.current.style.border = '2px solid #dc3545';
fieldRef.current.style.boxShadow = '0 0 0 0.2rem rgba(220, 53, 69, 0.25)';
setTimeout(() => {
if (fieldRef.current) {
fieldRef.current.style.border = "";
fieldRef.current.style.boxShadow = "";
fieldRef.current.style.border = '';
fieldRef.current.style.boxShadow = '';
}
}, 3000);
}
@ -204,17 +210,18 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const handleSubmit = async () => {
const missingFields = [];
if (!formData.full_name) missingFields.push("full_name");
if (!formData.cpf) missingFields.push("cpf");
if (!formData.email) missingFields.push("email");
if (!formData.phone_mobile) missingFields.push("phone_mobile");
if (!formData.crm_uf) missingFields.push("crm_uf");
if (!formData.crm) missingFields.push("crm");
if (!formData.full_name) missingFields.push('full_name');
if (!formData.cpf) missingFields.push('cpf');
if (!formData.email) missingFields.push('email');
if (!formData.phone_mobile) missingFields.push('phone_mobile');
if (!formData.crm_uf) missingFields.push('crm_uf');
if (!formData.crm) missingFields.push('crm');
if (missingFields.length > 0) {
setEmptyFields(missingFields);
setShowRequiredModal(true);
setTimeout(() => {
if (missingFields.length > 0) {
scrollToEmptyField(missingFields[0]);
@ -223,23 +230,26 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
return;
}
const cpfLimpo = formData.cpf.replace(/\D/g, "");
const cpfLimpo = formData.cpf.replace(/\D/g, '');
if (cpfLimpo.length !== 11) {
setShowRequiredModal(true);
setEmptyFields(["cpf"]);
setCpfError("CPF deve ter 11 dígitos");
setTimeout(() => scrollToEmptyField("cpf"), 500);
setEmptyFields(['cpf']);
setCpfError('CPF deve ter 11 dígitos');
setTimeout(() => scrollToEmptyField('cpf'), 500);
return;
}
if (!validarCPF(formData.cpf)) {
setShowRequiredModal(true);
setEmptyFields(["cpf"]);
setCpfError("CPF inválido");
setTimeout(() => scrollToEmptyField("cpf"), 500);
setEmptyFields(['cpf']);
setCpfError('CPF inválido');
setTimeout(() => scrollToEmptyField('cpf'), 500);
return;
}
try {
await onSave({ ...formData });
@ -290,16 +300,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
alignItems: "center",
}}
>
<h5
style={{
color: "#fff",
margin: 0,
fontSize: "1.2rem",
fontWeight: "bold",
}}
>
Atenção
</h5>
<h5 style={{ color: "#fff", margin: 0, fontSize: "1.2rem", fontWeight: "bold" }}>Atenção</h5>
<button
onClick={handleModalClose}
style={{
@ -315,109 +316,20 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</div>
<div style={{ padding: "25px 20px" }}>
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: "0 0 15px 0",
fontWeight: "bold",
}}
>
{cpfError ? "Problema com o CPF:" : "Por favor, preencha:"}
<p style={{ color: "#111", fontSize: "1.1rem", margin: "0 0 15px 0", fontWeight: "bold" }}>
{cpfError ? 'Problema com o CPF:' : 'Por favor, preencha:'}
</p>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
marginLeft: "10px",
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginLeft: '10px' }}>
{cpfError ? (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
{cpfError}
</p>
<p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>{cpfError}</p>
) : (
<>
{!formData.full_name && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- Nome
</p>
)}
{!formData.cpf && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- CPF
</p>
)}
{!formData.email && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- Email
</p>
)}
{!formData.phone_mobile && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- Telefone
</p>
)}
{!formData.crm_uf && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- Estado do CRM
</p>
)}
{!formData.crm && (
<p
style={{
color: "#111",
fontSize: "1.1rem",
margin: 0,
fontWeight: "600",
}}
>
- CRM
</p>
)}
{!formData.full_name && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- Nome</p>}
{!formData.cpf && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- CPF</p>}
{!formData.email && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- Email</p>}
{!formData.phone_mobile && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- Telefone</p>}
{!formData.crm_uf && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- Estado do CRM</p>}
{!formData.crm && <p style={{ color: "#111", fontSize: "1.1rem", margin: 0, fontWeight: "600" }}>- CRM</p>}
</>
)}
</div>
@ -452,26 +364,18 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
)}
<div className="card p-3 shadow-sm">
<h3 className="mb-4 text-center" style={{ fontSize: "2.5rem" }}>
MediConnect
</h3>
<h3 className="mb-4 text-center" style={{ fontSize: '2.5rem' }}>MediConnect</h3>
<div className="mb-5 p-4 border rounded shadow-sm">
<h4
className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse("dadosPessoais")}
style={{ fontSize: "1.8rem" }}
>
<h4 className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse('dadosPessoais')}
style={{ fontSize: '1.8rem' }}>
Dados Pessoais
<span className="fs-5">
{collapsedSections.dadosPessoais ? "▲" : "▼"}
{collapsedSections.dadosPessoais ? '▲' : '▼'}
</span>
</h4>
<div
className={`collapse${
collapsedSections.dadosPessoais ? " show" : ""
}`}
>
<div className={`collapse${collapsedSections.dadosPessoais ? ' show' : ''}`}>
<div className="row mt-3">
<div className="col-md-6 mb-3 d-flex align-items-center">
<div className="me-3">
@ -479,25 +383,20 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
<img
src={avatarUrl}
alt="Avatar do Médico"
style={{
width: "100px",
height: "100px",
borderRadius: "50%",
objectFit: "cover",
}}
style={{ width: '100px', height: '100px', borderRadius: '50%', objectFit: 'cover' }}
/>
) : (
<div
style={{
width: "100px",
height: "100px",
borderRadius: "50%",
backgroundColor: "#e0e0e0",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "3.5rem",
color: "#9e9e9e",
width: '100px',
height: '100px',
borderRadius: '50%',
backgroundColor: '#e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '3.5rem',
color: '#9e9e9e'
}}
>
&#x2624;
@ -505,13 +404,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
)}
</div>
<div>
<label
htmlFor="foto-input"
className="btn btn-primary"
style={{ fontSize: "1rem" }}
>
Carregar Foto
</label>
<label htmlFor="foto-input" className="btn btn-primary" style={{ fontSize: '1rem' }}>Carregar Foto</label>
<input
type="file"
className="form-control d-none"
@ -520,66 +413,49 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
onChange={handleChange}
accept="image/*"
/>
{formData.foto && (
<span className="ms-2" style={{ fontSize: "1rem" }}>
{formData.foto.name}
</span>
)}
{formData.foto && <span className="ms-2" style={{ fontSize: '1rem' }}>{formData.foto.name}</span>}
</div>
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Nome: *</label>
<label style={{ fontSize: '1.1rem' }}>Nome: *</label>
<input
ref={nomeRef}
type="text"
className="form-control"
name="full_name"
value={formData.full_name || ""}
value={formData.full_name || ''}
onChange={handleChange}
/>
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>
Data de nascimento:
</label>
<input
type="date"
className="form-control"
name="birth_date"
value={formData.birth_date || ""}
onChange={handleChange}
min="1900-01-01"
max="2025-09-24"
/>
<label style={{ fontSize: '1.1rem' }}>Data de nascimento:</label>
<input type="date" className="form-control" name="birth_date" value={formData.birth_date || ''} onChange={handleChange} min="1900-01-01" max="2025-09-24" />
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>CPF: *</label>
<label style={{ fontSize: '1.1rem' }}>CPF: *</label>
<input
ref={cpfRef}
type="text"
className={`form-control ${cpfError ? "is-invalid" : ""}`}
className={`form-control ${cpfError ? 'is-invalid' : ''}`}
name="cpf"
value={formData.cpf || ""}
value={formData.cpf || ''}
onChange={handleChange}
/>
{cpfError && (
<div
className="invalid-feedback"
style={{ display: "block" }}
>
<div className="invalid-feedback" style={{ display: 'block' }}>
{cpfError}
</div>
)}
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Estado do CRM: *</label>
<label style={{ fontSize: '1.1rem' }}>Estado do CRM: *</label>
<select
ref={crmUfRef}
className="form-control"
name="crm_uf"
value={formData.crm_uf || ""}
value={formData.crm_uf || ''}
onChange={handleChange}
>
<option value="">Selecione</option>
@ -613,37 +489,28 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>CRM: *</label>
<label style={{ fontSize: '1.1rem' }}>CRM: *</label>
<input
ref={crmRef}
type="text"
className="form-control"
name="crm"
value={formData.crm || ""}
value={formData.crm || ''}
onChange={handleChange}
/>
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Especialização:</label>
<select
className="form-control"
name="specialty"
value={formData.specialty || ""}
onChange={handleChange}
>
<label style={{ fontSize: '1.1rem' }}>Especialização:</label>
<select className="form-control" name="specialty" value={formData.specialty || ''} onChange={handleChange}>
<option value="">Selecione</option>
<option value="Clínica Geral">
Clínica médica (clínico geral)
</option>
<option value="Clínica Geral">Clínica médica (clínico geral)</option>
<option value="Pediatria">Pediatria</option>
<option value="Ginecologia">Ginecologia e obstetrícia</option>
<option value="Cardiologia">Cardiologia</option>
<option value="Ortopedia">Ortopedia e traumatologia</option>
<option value="Oftalmologia">Oftalmologia</option>
<option value="Otorrinolaringologia">
Otorrinolaringologia
</option>
<option value="Otorrinolaringologia">Otorrinolaringologia</option>
<option value="Dermatologia">Dermatologia</option>
<option value="Neurologia">Neurologia</option>
<option value="Psiquiatria">Psiquiatria</option>
@ -657,187 +524,103 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</div>
<div className="mb-5 p-4 border rounded shadow-sm">
<h4
className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse("contato")}
style={{ fontSize: "1.8rem" }}
>
<h4 className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse('contato')}
style={{ fontSize: '1.8rem' }}>
Contato
<span className="fs-5">
{collapsedSections.contato ? "▲" : "▼"}
{collapsedSections.contato ? '▲' : '▼'}
</span>
</h4>
<div
className={`collapse${collapsedSections.contato ? " show" : ""}`}
>
<div className={`collapse${collapsedSections.contato ? ' show' : ''}`}>
<div className="row mt-3">
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Email: *</label>
<label style={{ fontSize: '1.1rem' }}>Email: *</label>
<input
ref={emailRef}
type="email"
className="form-control"
name="email"
value={formData.email || ""}
value={formData.email || ''}
onChange={handleChange}
/>
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Telefone: *</label>
<label style={{ fontSize: '1.1rem' }}>Telefone: *</label>
<input
ref={telefoneRef}
type="text"
className="form-control"
name="phone_mobile"
value={formData.phone_mobile || ""}
value={formData.phone_mobile || ''}
onChange={handleChange}
/>
</div>
<div className="col-md-6 mb-3">
<label style={{ fontSize: "1.1rem" }}>Telefone 2:</label>
<input
type="text"
className="form-control"
name="phone2"
value={formData.phone2 || ""}
onChange={handleChange}
/>
<label style={{ fontSize: '1.1rem' }}>Telefone 2:</label>
<input type="text" className="form-control" name="phone2" value={formData.phone2 || ''} onChange={handleChange} />
</div>
</div>
</div>
</div>
<div className="mb-5 p-4 border rounded shadow-sm">
<h4
className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse("endereco")}
style={{ fontSize: "1.8rem" }}
>
<h4 className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse('endereco')}
style={{ fontSize: '1.8rem' }}>
Endereço
<span className="fs-5">
{collapsedSections.endereco ? "▲" : "▼"}
{collapsedSections.endereco ? '▲' : '▼'}
</span>
</h4>
<div
className={`collapse${collapsedSections.endereco ? " show" : ""}`}
>
<div className={`collapse${collapsedSections.endereco ? ' show' : ''}`}>
<div className="row mt-3">
<div className="col-md-4 mb-3">
<label>CEP:</label>
<input
type="text"
className="form-control"
name="cep"
value={formData.cep || ""}
onChange={handleChange}
onBlur={handleCepBlur}
/>
<input type="text" className="form-control" name="cep" value={formData.cep || ''} onChange={handleChange} onBlur={handleCepBlur} />
</div>
<div className="col-md-8 mb-3">
<label>Rua:</label>
<input
type="text"
className="form-control"
name="street"
value={formData.street || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="street" value={formData.street || ''} onChange={handleChange} />
</div>
<div className="col-md-6 mb-3">
<label>Bairro:</label>
<input
type="text"
className="form-control"
name="neighborhood"
value={formData.neighborhood || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="neighborhood" value={formData.neighborhood || ''} onChange={handleChange} />
</div>
<div className="col-md-4 mb-3">
<label>Cidade:</label>
<input
type="text"
className="form-control"
name="city"
value={formData.city || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="city" value={formData.city || ''} onChange={handleChange} />
</div>
<div className="col-md-2 mb-3">
<label>Estado:</label>
<input
type="text"
className="form-control"
name="state"
value={formData.state || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="state" value={formData.state || ''} onChange={handleChange} />
</div>
<div className="col-md-4 mb-3">
<label>Número:</label>
<input
type="text"
className="form-control"
name="number"
value={formData.number || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="number" value={formData.number || ''} onChange={handleChange} />
</div>
<div className="col-md-8 mb-3">
<label>Complemento:</label>
<input
type="text"
className="form-control"
name="complement"
value={formData.complement || ""}
onChange={handleChange}
/>
<input type="text" className="form-control" name="complement" value={formData.complement || ''} onChange={handleChange} />
</div>
</div>
</div>
</div>
<div className="mb-5 p-4 border rounded shadow-sm">
<h4
className="mb-4 cursor-pointer d-flex justify-content-between align-items-center"
onClick={() => handleToggleCollapse("disponibilidade")}
style={{ fontSize: "1.8rem" }}
>
Disponibilidade Semanal
<span className="fs-5">
{collapsedSections.disponibilidade ? "▲" : "▼"}
</span>
</h4>
<div
className={`collapse${
collapsedSections.disponibilidade ? " show" : ""
}`}
>
<WeeklyAvailabilityPicker
initialAvailability={formData.availability || {}}
onChange={(newAvailability) => {
setFormData((prev) => ({
...prev,
availability: newAvailability,
}));
}}
/>
</div>
</div>
<div className="mt-3 text-center">
<button
className="btn btn-success me-3"
onClick={handleSubmit}
disabled={isLoading}
style={{ fontSize: "1.2rem", padding: "0.75rem 1.5rem" }}
style={{ fontSize: '1.2rem', padding: '0.75rem 1.5rem' }}
>
{isLoading ? "Salvando..." : "Salvar Médico"}
{isLoading ? 'Salvando...' : 'Salvar Médico'}
</button>
<Link to={`/${location.pathname.split("/")[1]}/medicos`}>
<button
className="btn btn-light"
style={{ fontSize: "1.2rem", padding: "0.75rem 1.5rem" }}
style={{ fontSize: '1.2rem', padding: '0.75rem 1.5rem' }}
>
Cancelar
</button>
@ -848,79 +631,4 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
);
}
function WeeklyAvailabilityPicker() {
const days = ["Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"];
const [availability, setAvailability] = useState(
days.reduce((acc, day) => ({ ...acc, [day]: [] }), {})
);
const handleAddInterval = (day) => {
const newIntervals = [...availability[day], { start: "", end: "" }];
const newAvailability = { ...availability, [day]: newIntervals };
setAvailability(newAvailability);
};
const handleRemoveInterval = (day, index) => {
const newIntervals = availability[day].filter((_, i) => i !== index);
setAvailability({ ...availability, [day]: newIntervals });
};
const handleTimeChange = (day, index, field, value) => {
const newIntervals = availability[day].map((interval, i) =>
i === index ? { ...interval, [field]: value } : interval
);
setAvailability({ ...availability, [day]: newIntervals });
};
const handleSave = () => {
const data = [];
for (const [day, intervals] of Object.entries(availability)) {
intervals.forEach(({ start, end }) => {
const dayIndex = days.indexOf(day);
data.push({ day: dayIndex, start, end });
});
}
fetch("https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((res) => console.log("Salvo:", res))
.catch((err) => console.error("Erro:", err));
};
return (
<div>
{days.map((day) => (
<div key={day} style={{ marginBottom: "20px" }}>
<h5>{day}</h5>
{availability[day].map((interval, index) => (
<div
key={index}
style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "5px" }}
>
<input
type="time"
value={interval.start}
onChange={(e) => handleTimeChange(day, index, "start", e.target.value)}
/>
<span>até</span>
<input
type="time"
value={interval.end}
onChange={(e) => handleTimeChange(day, index, "end", e.target.value)}
/>
<button onClick={() => handleRemoveInterval(day, index)}>Remover</button>
</div>
))}
<button onClick={() => handleAddInterval(day)}>Adicionar intervalo</button>
</div>
))}
<button onClick={handleSave}>Salvar Disponibilidade</button>
</div>
);
}
export default DoctorForm;

View File

@ -4,8 +4,6 @@ import API_KEY from "../apiKeys";
const GetDoctorByID = async (ID,authHeader) => {
console.log(authHeader, 'mostrando autorização dentro da função')
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY)
myHeaders.append('Authorization', authHeader)
@ -23,4 +21,36 @@ return DictMedico
}
export {GetDoctorByID}
const GetAllDoctors = async (authHeader) => {
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
const result = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
const DictMedicos = await result.json()
return DictMedicos
}
const GetDoctorByName = async (nome, authHeader) => {
const Medicos = await GetAllDoctors(authHeader)
for (let i = 0; i < Medicos.length; i++) {
if (Medicos[i].full_name === nome) {
console.log('Medico encontrado:', Medicos[i]);
return Medicos[i];
}
else{console.log("nada encontrado")}
}
}
export {GetDoctorByID, GetDoctorByName}

View File

@ -4,8 +4,6 @@ import API_KEY from "../apiKeys";
const GetPatientByID = async (ID,authHeader) => {
console.log(authHeader, 'mostrando autorização dentro da função')
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY)
myHeaders.append('Authorization', authHeader)

View File

@ -0,0 +1,13 @@
[
{
"name": "Minhas consulta",
"icon": "calendar-plus-fill",
"url": "/paciente/agendamento"
},
{
"name": "Meus laudos",
"icon": "table",
"url": "/paciente/laudo"
}
]

View File

@ -1,14 +1,16 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys.js';
import AgendamentoCadastroManager from './AgendamentoCadastroManager.jsx';
import TabelaAgendamentoDia from '../components/AgendarConsulta/TabelaAgendamentoDia';
import TabelaAgendamentoSemana from '../components/AgendarConsulta/TabelaAgendamentoSemana';
import TabelaAgendamentoMes from '../components/AgendarConsulta/TabelaAgendamentoMes';
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta';
import { useAuth } from '../components/utils/AuthProvider.js';
// NOVO: Caminho de importação corrigido com base na sua estrutura de pastas
import AgendamentosMes from '../components/AgendarConsulta/DadosConsultasMock.js';
import dayjs from 'dayjs';
import "./style/Agendamento.css";
import './style/FilaEspera.css';
@ -20,6 +22,59 @@ const Agendamento = () => {
const [tabela, setTabela] = useState('diario');
const [PageNovaConsulta, setPageConsulta] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [agendamentos, setAgendamentos] = useState()
const {getAuthorizationHeader} = useAuth()
const [DictAgendamentosOrganizados, setAgendamentosOrganizados ] = useState({})
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [AgendamentoFiltrado, setAgendamentoFiltrado] = useState()
let authHeader = getAuthorizationHeader()
const FiltrarAgendamentos = (listaTodosAgendamentos) => {
let DictAgendamentosOrganizados = {};
for (let i = 0; i < listaTodosAgendamentos.length; i++) {
const agendamento = listaTodosAgendamentos[i];
const DiaAgendamento = agendamento.scheduled_at.split("T")[0];
console.log(DictAgendamentosOrganizados)
if (DiaAgendamento in DictAgendamentosOrganizados) {
// já existe a data adiciona na lista
DictAgendamentosOrganizados[DiaAgendamento].push(agendamento);
} else {
// não existe cria nova key com uma lista
DictAgendamentosOrganizados[DiaAgendamento] = [agendamento];
}
}
// faz o set de uma vez só
setAgendamentosOrganizados(DictAgendamentosOrganizados);
}
// Requisição inicial para mostrar os agendamentos do banco de dados
useEffect(() => {
var myHeaders = new Headers();
myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY)
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select&doctor_id&patient_id&status&scheduled_at&order&limit&offset", requestOptions)
.then(response => response.json())
.then(result => {FiltrarAgendamentos(result); console.log(result, "aqui")})
.catch(error => console.log('error', error));
}, [])
// Dados da fila de espera (sem alteração)
const filaEsperaData = [
@ -78,7 +133,7 @@ const Agendamento = () => {
default: break
}
}
let ListaDiasDatas = [segundas, tercas, quartas, quintas, sextas]
let ListaDiasDatas = {segundas:segundas,tercas:tercas,quartas: quartas,quintas: quintas,sextas: sextas}
return ListaDiasDatas
}
@ -175,9 +230,9 @@ const Agendamento = () => {
</div>
</section>
{tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={filteredAgendamentos} />}
{tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={filteredAgendamentos} />}
{tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={filteredAgendamentos} />}
{tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} />}
{tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={DictAgendamentosOrganizados} ListarDiasdoMes={ListarDiasdoMes}/>}
{tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={DictAgendamentosOrganizados} />}
</div>
</div>
)
@ -222,8 +277,65 @@ const Agendamento = () => {
</section>
</div>
) : (
<FormNovaConsulta onCancel={handleClickCancel} />
<AgendamentoCadastroManager />
)}
{showDeleteModal && (
<div
className="modal fade show"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
onClick={(e) =>
e.target.classList.contains("modal") && setShowDeleteModal(false)
}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir este paciente?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
>
Cancelar
</button>
<button
type="button"
className="btn btn-danger"
//onClick={() => deletePatient(selectedPatientId)}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
</div>
</div>
</div>
</div>)}
</div>
)
}

View File

@ -0,0 +1,58 @@
import React from 'react'
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
import { useState } from 'react'
import dayjs from 'dayjs'
const AgendamentoCadastroManager = () => {
const {getAuthorizationHeader} = useAuth()
const [agendamento, setAgendamento] = useState({})
let authHeader = getAuthorizationHeader()
const handleSave = (Dict) => {
let DataAtual = dayjs()
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({
"patient_id": Dict.patient_id,
"doctor_id": Dict.doctor_id,
"scheduled_at": DataAtual,
"duration_minutes": 30,
"appointment_type": "presencial",
"chief_complaint": "Dor de cabeça há 3 ",
"patient_notes": "Prefiro horário pela manhã",
"insurance_provider": "Unimed",
"created_by": "87f2662c-9da7-45c0-9e05-521d9d92d105"
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
return (
<div>
<FormNovaConsulta onSave={handleSave} agendamento={agendamento} setAgendamento={setAgendamento}/>
</div>
)
}
export default AgendamentoCadastroManager

View File

@ -0,0 +1,70 @@
import React from 'react'
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
import dayjs from 'dayjs'
const AgendamentoEditPage = () => {
let DataAtual = dayjs()
const {getAuthorizationHeader} = useAuth()
const params = useParams()
const [PatientToPatch, setPatientToPatch] = useState({})
let id = params.id
console.log(id)
let authHeader = getAuthorizationHeader()
const handleSave = (DictParaPatch) => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY)
myHeaders.append("authorization", authHeader)
console.log(DictParaPatch)
var raw = JSON.stringify({"patient_id": DictParaPatch.patient_id,
"doctor_id": DictParaPatch.doctor_id,
"scheduled_at": DataAtual,
"duration_minutes": 30,
"appointment_type": "presencial",
"chief_complaint": "Dor de cabeça há 3 ",
"patient_notes": "Prefiro horário pela manhã",
"insurance_provider": "Unimed",
"created_by": "87f2662c-9da7-45c0-9e05-521d9d92d105"
});
console.log(DictParaPatch)
console.log(id)
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
return (
<div>
<FormNovaConsulta onSave={handleSave} agendamento={PatientToPatch} setAgendamento={setPatientToPatch}/>
</div>
)
}
export default AgendamentoEditPage

View File

@ -44,7 +44,6 @@ function DoctorCadastroManager() {
number: doctorData.number || null,
complement: doctorData.complement || null,
phone2: doctorData.phone2 ? doctorData.phone2.replace(/\D/g, '') : null,
availability: doctorData.availability || {},
};
console.log('Dados limpos para envio:', cleanedData);

View File

@ -1,115 +1,77 @@
import React, { useEffect, useState } from "react";
import { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import DoctorForm from "../components/doctors/DoctorForm";
import { useAuth } from "../components/utils/AuthProvider";
import { useParams } from "react-router-dom";
import API_KEY from "../components/utils/apiKeys";
import React from 'react'
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor'
import DoctorForm from '../components/doctors/DoctorForm'
import { useAuth } from '../components/utils/AuthProvider'
import {useEffect, useState} from 'react'
import { useParams } from 'react-router-dom'
import API_KEY from '../components/utils/apiKeys'
const DoctorEditPage = () => {
const { getAuthorizationHeader } = useAuth();
const [DoctorToPUT, setDoctorPUT] = useState({});
const [availability, setAvailability] = useState([]);
const { id: DoctorID } = useParams();
const {getAuthorizationHeader, isAuthenticated} = useAuth();
const [DoctorToPUT, setDoctorPUT] = useState({})
const Parametros = useParams()
const DoctorID = Parametros.id
useEffect(() => {
const authHeader = getAuthorizationHeader();
// Buscar médico
const authHeader = getAuthorizationHeader()
GetDoctorByID(DoctorID, authHeader)
.then((data) => setDoctorPUT(data[0]))
.catch((err) => console.error(err));
.then((data) => {
console.log(data, "médico vindo da API");
setDoctorPUT(data[0])
; // supabase retorna array
})
.catch((err) => console.error("Erro ao buscar paciente:", err));
// Buscar disponibilidades
const fetchAvailability = async () => {
try {
const res = await fetch(
`https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability/${DoctorID}`,
{ headers: { apikey: API_KEY, Authorization: authHeader } }
);
const data = await res.json();
setAvailability(data.data || []);
} catch (err) {
console.error(err);
}
};
fetchAvailability();
}, []);
// Atualizar uma disponibilidade
const updateAvailability = async (id, updatedData) => {
const authHeader = getAuthorizationHeader();
try {
await fetch(
`https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability/${id}`,
{
method: "PUT",
headers: {
apikey: API_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(updatedData),
}
);
// atualizar localmente
setAvailability((prev) =>
prev.map((a) => (a.id === id ? { ...a, ...updatedData } : a))
);
} catch (err) {
console.error(err);
}
};
// Deletar uma disponibilidade
const deleteAvailability = async (id) => {
const authHeader = getAuthorizationHeader();
try {
await fetch(
`https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability/${id}`,
{
method: "DELETE",
headers: { apikey: API_KEY, Authorization: authHeader },
}
);
setAvailability((prev) => prev.filter((a) => a.id !== id));
} catch (err) {
console.error(err);
}
};
}, [])
const HandlePutDoctor = async () => {
const authHeader = getAuthorizationHeader();
try {
await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DoctorID}`,
{
method: "PUT",
headers: {
apikey: API_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(DoctorToPUT),
}
);
} catch (err) {
console.error(err);
}
const authHeader = getAuthorizationHeader()
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY)
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify(DoctorToPUT);
console.log("Enviando médico para atualização:", DoctorToPUT);
var requestOptions = {
method: 'PUT',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
try {
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DoctorID}`,requestOptions);
console.log(response)
} catch (error) {
console.error("Erro ao atualizar paciente:", error);
throw error;
}
}
return (
<div>
<DoctorForm
onSave={HandlePutDoctor}
formData={DoctorToPUT}
setFormData={setDoctorPUT}
availability={availability}
updateAvailability={updateAvailability}
deleteAvailability={deleteAvailability}
/>
</div>
);
};
export default DoctorEditPage;
/>
</div>
)
}
export default DoctorEditPage

View File

@ -71,7 +71,7 @@ function TableDoctor() {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
.then(response => response.json())
.then(result => setMedicos(result))
.then(result => {setMedicos(result); console.log(result)})
.catch(error => console.log('error', error));
}, []);

View File

@ -171,7 +171,6 @@ function PatientCadastroManager( {setCurrentPage} ) {
});
setShowModal(true);
setTimeout(() => {
setShowModal(false);
navigate('/secretaria/pacientes');

107
src/pages/ProfilePage.jsx Normal file
View File

@ -0,0 +1,107 @@
// src/pages/ProfilePage.jsx
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import "./style/ProfilePage.css";
const simulatedUserData = {
email: "admin@squad23.com",
role: "Administrador",
};
const ProfilePage = () => {
const location = useLocation();
const navigate = useNavigate();
const getRoleFromPath = () => {
const path = location.pathname;
if (path.includes("/admin")) return "Administrador";
if (path.includes("/secretaria")) return "Secretária";
if (path.includes("/medico")) return "Médico";
if (path.includes("/financeiro")) return "Financeiro";
return "Usuário Padrão";
};
const userRole = simulatedUserData.role || getRoleFromPath();
const userEmail = simulatedUserData.email || "email.nao.encontrado@example.com";
const [userName, setUserName] = useState("Admin Padrão");
const [isEditingName, setIsEditingName] = useState(false);
const handleNameKeyDown = (e) => {
if (e.key === "Enter") setIsEditingName(false);
};
const handleClose = () => navigate(-1);
return (
<div className="profile-overlay" role="dialog" aria-modal="true">
<div className="profile-modal">
<button
className="profile-close"
onClick={handleClose}
aria-label="Fechar Perfil"
>
×
</button>
<div className="profile-content">
<div className="profile-left">
<div className="avatar-wrapper">
<div className="avatar-square" />
<button
className="avatar-edit-btn"
title="Editar foto"
aria-label="Editar foto"
type="button"
>
</button>
</div>
</div>
<div className="profile-right">
<div className="profile-name-row">
{isEditingName ? (
<input
className="profile-name-input"
value={userName}
onChange={(e) => setUserName(e.target.value)}
onBlur={() => setIsEditingName(false)}
onKeyDown={handleNameKeyDown}
autoFocus
/>
) : (
<h2 className="profile-username">{userName}</h2>
)}
<button
className="profile-edit-inline"
onClick={() => setIsEditingName(!isEditingName)}
aria-label="Editar nome"
type="button"
>
</button>
</div>
<p className="profile-email">
Email: <strong>{userEmail}</strong>
</p>
<p className="profile-role">
Cargo: <strong>{userRole}</strong>
</p>
<div className="profile-actions-row">
<button className="btn btn-close" onClick={handleClose}>
Fechar
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default ProfilePage;

10
src/pages/Support.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
export default function Support() {
return (
<div style={{padding:40}}>
<h3>Suporte por telefone</h3>
<p>Funcionalidade de chamada/suporte será implementada em breve.</p>
</div>
);
}

View File

@ -102,7 +102,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
console.log(authHeader)
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
@ -115,7 +115,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => response.json())
.then(result => setPacientes(result))
.then(result => {setPacientes(result); console.log(result)})
.catch(error => console.log('error', error));
}, [isAuthenticated, getAuthorizationHeader]);

View File

@ -118,7 +118,7 @@
border-radius: 10px;
}
#status-card-consulta-confirmado, .legenda-item-confirmado {
#status-card-consulta-confirmado, .legenda-item-confirmed {
background-color: #eef8fb;
border:3px solid #d8dfe7;
padding: 5px;
@ -289,3 +289,72 @@ html[data-bs-theme="dark"] {
background-color: #005a9e;
}
}
/* Estilo para o botão de Editar */
.btn-edit-custom {
background-color: #FFF3CD;
color: #856404;
}
/* Estilo para o botão de Excluir (Deletar) */
.btn-delete-custom {
background-color: #F8D7DA;
color: #721C24;
padding: 10px;
}
.cardconsulta{
display:flex;
align-items: center;
flex-direction: row;
}
.container-botons{
display: flex;
flex-direction: row;
}
#tabela-seletor-container {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
background-color: #fff;
border-radius: 8px;
padding: 6px 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
width: fit-content;
margin: 0 auto;
}
#tabela-seletor-container p {
margin: 0;
font-size: 23px;
font-weight: 500;
color: #4085f6;
text-align: center;
white-space: nowrap;
}
#tabela-seletor-container button {
background: transparent;
border: none;
color: #555;
font-size: 20px;
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
transition: all 0.2s ease-in-out;
}
#tabela-seletor-container button:hover {
background-color: rgba(0, 0, 0, 0.05);
color: #000;
}
#tabela-seletor-container i {
pointer-events: none;
}

View File

@ -0,0 +1,178 @@
/* src/pages/ProfilePage.css */
/* Overlay que cobre toda a tela */
.profile-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.65);
display: flex;
align-items: center;
justify-content: center;
z-index: 20000; /* acima de header, vlibras, botões de acessibilidade */
padding: 20px;
box-sizing: border-box;
}
/* Card central (estilo modal amplo parecido com a 4ª foto) */
.profile-modal {
background: #ffffff;
border-radius: 10px;
padding: 18px;
width: min(1100px, 96%);
max-width: 1100px;
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);
position: relative;
box-sizing: border-box;
overflow: visible;
}
/* Botão fechar (X) no canto do card */
.profile-close {
position: absolute;
top: 14px;
right: 14px;
background: none;
border: none;
font-size: 26px;
color: #666;
cursor: pointer;
line-height: 1;
}
/* Conteúdo dividido em 2 colunas: esquerda avatar / direita infos */
.profile-content {
display: flex;
gap: 28px;
align-items: flex-start;
padding: 22px 18px;
}
/* Coluna esquerda - avatar */
.profile-left {
width: 220px;
display: flex;
justify-content: center;
}
/* Avatar quadrado com sombra (estilo da foto 4) */
.avatar-wrapper {
position: relative;
width: 180px;
height: 180px;
}
.avatar-square {
width: 100%;
height: 100%;
border-radius: 8px;
background-color: #d0d0d0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'><path fill='%23FFFFFF' d='M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3c0 16.7 13.5 30.2 30.2 30.2h387.6c16.7 0 30.2-13.5 30.2-30.2 0-98.5-79.8-178.3-178.3-178.3h-45.7z'/></svg>");
background-position: center;
background-repeat: no-repeat;
background-size: 55%;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
}
/* Botão editar sobre o avatar — círculo pequeno */
.avatar-edit-btn {
position: absolute;
right: -8px;
bottom: -8px;
transform: translate(0, 0);
border: none;
background: #ffffff;
padding: 8px 9px;
border-radius: 50%;
box-shadow: 0 6px 14px rgba(0,0,0,0.18);
cursor: pointer;
font-size: 0.95rem;
line-height: 1;
}
/* Coluna direita - informações */
.profile-right {
flex: 1;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Nome e botão de editar inline */
.profile-name-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.profile-username {
margin: 0;
font-size: 1.9rem;
color: #222;
}
.profile-edit-inline {
background: none;
border: none;
cursor: pointer;
font-size: 1.05rem;
color: #444;
}
/* input de edição do nome */
.profile-name-input {
font-size: 1.6rem;
padding: 6px 8px;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
/* email/role */
.profile-email,
.profile-role {
margin: 6px 0;
color: #555;
font-size: 1rem;
}
.profile-role {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #f1f1f1;
color: #333;
}
/* ações (apenas fechar aqui) */
.profile-actions-row {
display: flex;
gap: 12px;
margin-top: 18px;
}
/* botões */
.btn {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
font-size: 0.95rem;
}
.btn-close {
background: #f0f0f0;
color: #222;
border: 1px solid #e6e6e6;
}
/* responsividade */
@media (max-width: 880px) {
.profile-content {
flex-direction: column;
gap: 14px;
align-items: center;
}
.profile-left { width: 100%; }
.avatar-wrapper { width: 140px; height: 140px; }
.profile-right { width: 100%; text-align: center; }
}

View File

@ -1,62 +0,0 @@
.container-perfis {
position: absolute;
top: 80px;
left: 30px;
width: calc(100% - 60px);
display: flex;
flex-direction: column;
align-items: flex-start;
z-index: 60;
}
.acesso-text {
font-size: 15px;
font-weight: 600;
color: #1e2b57;
margin-bottom: 6px;
}
/* estilo visual refinado do select */
.perfil-select {
width: 100%;
padding: 10px 14px;
border-radius: 10px;
border: 1.8px solid #d0d5dd;
background-color: #f9fafc;
color: #1e2b57;
font-weight: 600;
font-size: 14px;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(30, 43, 87, 0.08);
}
.perfil-select:hover {
border-color: #7a85ff;
background-color: #ffffff;
box-shadow: 0 2px 6px rgba(30, 43, 87, 0.1);
}
.perfil-select:focus {
border-color: #5a46ff;
box-shadow: 0 0 0 3px rgba(90, 70, 255, 0.2);
}
.perfil-select option[value=""] {
color: #777;
font-weight: 500;
}
/* responsivo */
@media (max-width: 780px) {
.container-perfis {
top: 60px;
left: 20px;
width: calc(100% - 40px);
}
.perfil-select {
font-size: 13px;
padding: 8px 12px;
}
}

View File

@ -0,0 +1,23 @@
import { Routes, Route } from "react-router-dom";
import Sidebar from "../../components/Sidebar";
import PacienteItems from "../../data/sidebar-items-paciente.json";
import Agendamento from "../../pages/Agendamento";
import LaudoManager from "../../pages/LaudoManager";
function PerfilPaciente({ onLogout }) {
return (
<div id="app" className="active">
<Sidebar onLogout={onLogout} menuItems={PacienteItems} />
<div id="main">
<Routes>
<Route path="/" element={<LaudoManager />} />
<Route path="agendamento" element={<Agendamento />} />
<Route path="laudo" element={<LaudoManager />} />
<Route path="*" element={<h2>Página não encontrada</h2>} />
</Routes>
</div>
</div>
);
}
export default PerfilPaciente;

View File

@ -16,6 +16,7 @@ import EditPage from "../../pages/EditPage";
import DoctorDetails from "../../pages/DoctorDetails";
import DoctorEditPage from "../../pages/DoctorEditPage";
import FormDisponibilidade from "../../components/AgendarConsulta/FormDisponibilidade";
import AgendamentoEditPage from "../../pages/AgendamentoEditPage";
function PerfilSecretaria({ onLogout }) {
return (
@ -34,6 +35,7 @@ function PerfilSecretaria({ onLogout }) {
<Route path="medicos/:id" element={<DoctorDetails />} />
<Route path="medicos/:id/edit" element={<DoctorEditPage />} />
<Route path="agendamento" element={<Agendamento />} />
<Route path="agendamento/:id/edit" element={<AgendamentoEditPage/>} />
<Route path="laudo" element={<LaudoManager />} />
<Route path="*" element={<h2>Página não encontrada</h2>} />
<Route path="form-disponibilidade" element={<FormDisponibilidade />} />