Merge branch 'main' into ExcecoesFinal

This commit is contained in:
pedrofedericoo 2025-10-15 21:35:39 -03:00
commit a4d7fae123
30 changed files with 2363 additions and 475 deletions

746
package-lock.json generated
View File

@ -15,6 +15,9 @@
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.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", "apexcharts": "^5.3.4",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
@ -16184,6 +16187,31 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "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": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -17174,6 +17202,11 @@
"url": "https://opencollective.com/immer" "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": { "node_modules/@restart/hooks": {
"version": "0.4.16", "version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
@ -17747,6 +17780,424 @@
"@testing-library/dom": ">=7.21.4" "@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": { "node_modules/@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -18078,6 +18529,20 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"license": "MIT" "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": { "node_modules/@types/mdast": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@ -18088,6 +18553,11 @@
"@types/unist": "*" "@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": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -18184,14 +18654,22 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.12", "version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "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": { "node_modules/@types/react-transition-group": {
"version": "4.4.12", "version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@ -21060,6 +21538,11 @@
"node": ">=10" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -26850,6 +27333,19 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT" "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": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@ -27041,6 +27537,38 @@
"tmpl": "1.0.5" "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": { "node_modules/markdown-table": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@ -27329,6 +27857,11 @@
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
"license": "CC0-1.0" "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": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -28526,6 +29059,11 @@
"node": ">= 0.8.0" "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": { "node_modules/own-keys": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@ -30322,6 +30860,183 @@
"url": "https://github.com/sponsors/wooorm" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -30365,6 +31080,14 @@
"node": ">=6" "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": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -31707,6 +32430,11 @@
"randombytes": "^2.1.0" "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": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -33802,6 +34530,11 @@
"node": ">=4.2.0" "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": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@ -34250,6 +34983,11 @@
"browser-process-hrtime": "^1.0.0" "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": { "node_modules/w3c-xmlserializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", "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/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.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", "apexcharts": "^5.3.4",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",

View File

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

View File

@ -1,63 +1,170 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import FormRelatorio from '../components/FormRelatorio' import { useParams, useNavigate } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import API_KEY from '../components/utils/apiKeys' import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider' import { useAuth } from '../components/utils/AuthProvider'
import TiptapEditor from '../PagesMedico/TiptapEditor'
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
const EditPageRelatorio = () => { const EditPageRelatorio = () => {
const params = useParams() const params = useParams()
const navigate = useNavigate()
const {getAuthorizationHeader} = useAuth() const {getAuthorizationHeader} = useAuth()
let authHeader = getAuthorizationHeader() 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(); var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY); myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader); myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
const raw = JSON.stringify({...RelatorioInfos, order_number:'REL-2025-4386'}) // Salva apenas o novo conteúdo do Tiptap (relatorioData.content)
const raw = JSON.stringify({
console.log(RelatorioInfos) 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 = { var requestOptions = {
method: 'PATCH', method: 'PATCH',
headers: myHeaders, headers: myHeaders,
body: raw, body: raw,
redirect: 'follow' redirect: 'follow'
};
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${RelatorioID}`, requestOptions) fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${RelatorioID}`, requestOptions)
.then(response => response.text()) .then(response => response.text())
.then(result => console.log(result)) .then(result => {
.catch(error => console.log('error', error)); 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(() => { useEffect(() => {
const fetchReportData = async () => {
var myHeaders = new Headers(); var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY); myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader); myHeaders.append("Authorization", authHeader);
var requestOptions = { var requestOptions = {
method: 'GET', method: 'GET',
headers: myHeaders, headers: myHeaders,
redirect: 'follow' 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) if (result.length > 0) {
.then(response => response.json()) report = result[0];
.then(result => setDictInfo(result[0]))
.catch(error => console.log('error', error));
}, [])
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 ( return (
<div> <div className='container'>
<FormRelatorio DictInfo={DictInfo} setDictInfo={setDictInfo} onSave={handleSave}/> {/* 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> </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 ( return (
<div className={`container-cardconsulta-${TabelaAgendamento}`}> <div className={`container-cardconsulta-${TabelaAgendamento}`}>
{DadosConsulta.status !== 'vazio'? {DadosConsulta.id?
<div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}> <div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}>
<div>
<section className='cardconsulta-infosecundaria'> <section className='cardconsulta-infosecundaria'>
<p>{DadosConsulta.horario}|GEAP| {DadosConsulta.medico}</p> <p>{DadosConsulta.horario} {Medico?.full_name}</p>
</section> </section>
<section className='cardconsulta-infoprimaria'> <section className='cardconsulta-infoprimaria'>
<p>{DadosConsulta.paciente} - {DadosConsulta.motivo} - 23 anos</p> <p>{Paciente?.full_name} - {DadosConsulta.exam}</p>
</section> </section>
</div> </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 null

View File

@ -1,57 +1,21 @@
import InputMask from "react-input-mask"; import InputMask from "react-input-mask";
import "./style/formagendamentos.css"; import "./style/formagendamentos.css";
import { useState, useEffect } from "react"; 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, onSave, setAgendamento, agendamento }) => {
const FormNovaConsulta = ({ onCancel, patientID }) => { const {getAuthorizationHeader} = useAuth()
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const [anexos, setAnexos] = useState([]); const [anexos, setAnexos] = useState([]);
const [loadingAnexos, setLoadingAnexos] = useState(false); const [loadingAnexos, setLoadingAnexos] = useState(false);
const [paciente, setPaciente] = useState({})
const [acessibilidade, setAcessibilidade] = useState({cadeirante:false,idoso:false,gravida:false,bebe:false, autista:false }) const [acessibilidade, setAcessibilidade] = useState({cadeirante:false,idoso:false,gravida:false,bebe:false, autista:false })
useEffect(() => { let authHeader = getAuthorizationHeader()
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]);
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) => { const handleclickAcessibilidade = (id) => {
@ -65,11 +29,7 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
const FormatCPF = (valor) => { const FormatCPF = (valor) => {
console.log(valor)
const digits = String(valor).replace(/\D/g, '').slice(0, 11); const digits = String(valor).replace(/\D/g, '').slice(0, 11);
BuscarPacienteExistentePeloCPF(valor)
return digits return digits
.replace(/(\d{3})(\d)/, '$1.$2') .replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2') .replace(/(\d{3})(\d)/, '$1.$2')
@ -77,97 +37,54 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
} }
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);
console.log("Resultado:", result);
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));
}
} catch (error) {
console.log("error", error);
}
//BuscarCPFnoBancodeDados(value)
}
const handleChange = (e) => { const handleChange = (e) => {
const {value, name} = e.target; const {value, name} = e.target;
console.log(value, name)
if(name === 'email'){ if(name === 'email'){
setPaciente({...paciente, contato:{ setAgendamento({...agendamento, contato:{
...paciente.contato, ...agendamento.contato,
email:value email:value
}}) }})
} else if(name === 'telefone'){ }else if(name === 'cpf'){
setPaciente({...paciente, contato:{
...paciente.contato, let cpfFormatted = FormatCPF(value)
telefone1:FormatTelefones(value) const fetchPatient = async () => {
}}) let patientData = await GetPatientByCPF(cpfFormatted, authHeader);
if (patientData) {
setAgendamento((prev) => ({
...prev,
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 fetchDoctor = async () => {
let DoctorData = await GetDoctorByName(value, authHeader)
if(DoctorData){
setAgendamento((prev) => ({
...prev,
doctor_id:DoctorData.id
}))
}}
fetchDoctor()
} }
else{ else{
setPaciente({...paciente,[name]:value}) setAgendamento({...agendamento,[name]:value})
} }
} }
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
alert("Agendamento salvo!"); alert("Agendamento salvo!");
onSave(agendamento)
}; };
return ( return (
@ -180,57 +97,33 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
<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"> <div className="campo-de-input">
<label>Nome *</label> <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>
<div className="campo-de-input"> <div className="campo-de-input">
<label>CPF do paciente</label> <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>
<div className="campo-de-input">
<label>RG</label>
<input type="text" name="rg" placeholder="Insira o nº do RG" maxLength={9} />
</div>
</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"> <div className="campo-de-input">
<label>Convênio</label> <label>Convênio</label>
<select name="convenio"> <select name="convenio" onChange={handleChange}>
<option value="particular">Particular</option>
<option value="publico">Público</option> <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> </select>
</div> </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> </div>
<h3 className="section-subtitle">Informações adicionais</h3> <h3 className="section-subtitle">Informações adicionais</h3>
@ -243,7 +136,7 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
onChange={(e) => setSelectedFile(e.target.files[0])} onChange={(e) => setSelectedFile(e.target.files[0])}
/> />
{selectedFile && ( {selectedFile && (
<button type="button" className="btn btn-primary ms-2" onClick={handleUpload}> <button type="button" className="btn btn-primary ms-2" >
Enviar Enviar
</button> </button>
)} )}
@ -291,7 +184,7 @@ const FormNovaConsulta = ({ onCancel, patientID }) => {
<div className="campo-de-input"> <div className="campo-de-input">
<label>Nome do profissional *</label> <label>Nome do profissional *</label>
<input type="text" name="profissional" required /> <input type="text" name="profissional" onChange={handleChange} value={agendamento.nome_medico}required />
</div> </div>

View File

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

View File

@ -3,7 +3,8 @@ import React from 'react';
import dayjs from "dayjs"; import dayjs from "dayjs";
import CardConsulta from './CardConsulta'; import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabelames.css"; import "./style/styleTabelas/tabelames.css";
import { useEffect, useState } from 'react';
import { useMemo } from 'react';
const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => { const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
@ -12,19 +13,179 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
const mes = dataHoje.month() + 1; const mes = dataHoje.month() + 1;
let ListaDiasDatas = ListarDiasdoMes(AnoAtual, mes); 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 ( return (
<div> <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'> <table className='tabelamensal'>
<thead> <thead>
<tr> <tr>
<th>Seg</th> <td className='cabecalho-tabela'>Seg</td>
<th>Ter</th> <th>Ter</th>
<th>Qua</th> <th>Qua</th>
<th>Qui</th> <th>Qui</th>
@ -32,104 +193,32 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{agendamentos && Object.entries(agendamentos).map(([semana, dias], index) => ( {Object.keys(AgendamentosMensaisOrganizados[indice]).map((semanaKey) => {
<tr key={index}> const semana = AgendamentosMensaisOrganizados[indice][semanaKey]
{/* Coluna de Segunda-feira */} console.log(AgendamentosMensaisOrganizados[indice][semanaKey], 'ajdsahchbaohdfoduh')
<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>
{/* 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 */} return(
<td> <tr key={semanaKey}>
<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>
{/* 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> <div>
<p>{quintas[index]}</p> <p>{` +${semana[dia].length - 2}`}</p>
<div>
{(dias.quinta || []).slice(0, 3).map((consulta, idx) => (
<CardConsulta
key={idx}
DadosConsulta={consulta}
className={`usuario-${consulta.cor || "default"}`}
/>
))}
</div> </div>
{(dias.quinta || []).length > 3 ?
<div><p className='cards-que-faltam'>+ {(dias.quinta || []).length - 3}</p></div>
: null}
</div>
</td>
{/* Coluna de Sexta-feira */} ): null }
<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>
</td> </td>
))
}
</tr> </tr>
))}
)})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,31 +1,150 @@
import React from 'react'; import React from 'react';
import CardConsulta from './CardConsulta'; import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabelasemana.css"; 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 || {}; // Extrai os agendamentos da semana atual (ou um objeto vazio se não existir)
const semanaParaRenderizar = semanasOrganizadas[chaveDaSemanaAtual] || {
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
const agendamentosDeSegunda = agendamentoSemana.segunda || []; };
const agendamentosDeTerca = agendamentoSemana.terca || [];
const agendamentosDeQuarta = agendamentoSemana.quarta || [];
const agendamentosDeQuinta = agendamentoSemana.quinta || [];
const agendamentosDeSexta = agendamentoSemana.sexta || [];
// Determina o número máximo de linhas/consultas
const numLinhas = Math.max( const numLinhas = Math.max(
agendamentosDeSegunda.length, semanaParaRenderizar.segunda.length,
agendamentosDeTerca.length, semanaParaRenderizar.terça.length,
agendamentosDeQuarta.length, semanaParaRenderizar.quarta.length,
agendamentosDeQuinta.length, semanaParaRenderizar.quinta.length,
agendamentosDeSexta.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 ( return (
<div> <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'> <table className='tabelasemanal'>
<thead> <thead>
<tr> <tr>
@ -38,28 +157,44 @@ const TabelaAgendamentoSemana = ({ agendamentos }) => {
</tr> </tr>
</thead> </thead>
<tbody> <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]; {/* Mapeamento de COLUNAS (dias) */}
const consultaTer = agendamentosDeTerca[index]; <td>
const consultaQua = agendamentosDeQuarta[index]; {semanaParaRenderizar.segunda[indiceLinha]
const consultaQui = agendamentosDeQuinta[index]; ? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} />
const consultaSex = agendamentosDeSexta[index]; : null
}
</td>
const horarioDaLinha = consultaSeg?.horario || consultaTer?.horario || consultaQua?.horario || consultaQui?.horario || consultaSex?.horario; <td>
{semanaParaRenderizar.terça[indiceLinha]
return ( ? <CardConsulta DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} />
<tr key={index}> : null
<td>{horarioDaLinha}</td> }
<td>{consultaSeg && <CardConsulta DadosConsulta={consultaSeg} />}</td> </td>
<td>{consultaTer && <CardConsulta DadosConsulta={consultaTer} />}</td> <td>
<td>{consultaQua && <CardConsulta DadosConsulta={consultaQua} />}</td> {semanaParaRenderizar.quarta[indiceLinha]
<td>{consultaQui && <CardConsulta DadosConsulta={consultaQui} />}</td> ? <CardConsulta DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} />
<td>{consultaSex && <CardConsulta DadosConsulta={consultaSex} />}</td> : 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> </tr>
); ))}
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -115,3 +115,8 @@ html[data-bs-theme="dark"] .mostrar-horario th {
color: #e0e0e0; color: #e0e0e0;
background-color: #232323; 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 { html[data-bs-theme="dark"] .cards-que-faltam {
color: #90caf9; 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

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

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 { 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 TabelaAgendamentoDia from '../components/AgendarConsulta/TabelaAgendamentoDia';
import TabelaAgendamentoSemana from '../components/AgendarConsulta/TabelaAgendamentoSemana'; import TabelaAgendamentoSemana from '../components/AgendarConsulta/TabelaAgendamentoSemana';
import TabelaAgendamentoMes from '../components/AgendarConsulta/TabelaAgendamentoMes'; import TabelaAgendamentoMes from '../components/AgendarConsulta/TabelaAgendamentoMes';
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'; 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 // NOVO: Caminho de importação corrigido com base na sua estrutura de pastas
import AgendamentosMes from '../components/AgendarConsulta/DadosConsultasMock.js'; import AgendamentosMes from '../components/AgendarConsulta/DadosConsultasMock.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import "./style/Agendamento.css"; import "./style/Agendamento.css";
import './style/FilaEspera.css'; import './style/FilaEspera.css';
@ -20,6 +22,59 @@ const Agendamento = () => {
const [tabela, setTabela] = useState('diario'); const [tabela, setTabela] = useState('diario');
const [PageNovaConsulta, setPageConsulta] = useState(false); const [PageNovaConsulta, setPageConsulta] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); 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) // Dados da fila de espera (sem alteração)
const filaEsperaData = [ const filaEsperaData = [
@ -78,7 +133,7 @@ const Agendamento = () => {
default: break default: break
} }
} }
let ListaDiasDatas = [segundas, tercas, quartas, quintas, sextas] let ListaDiasDatas = {segundas:segundas,tercas:tercas,quartas: quartas,quintas: quintas,sextas: sextas}
return ListaDiasDatas return ListaDiasDatas
} }
@ -175,9 +230,9 @@ const Agendamento = () => {
</div> </div>
</section> </section>
{tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={filteredAgendamentos} />} {tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} />}
{tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={filteredAgendamentos} />} {tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={DictAgendamentosOrganizados} ListarDiasdoMes={ListarDiasdoMes}/>}
{tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={filteredAgendamentos} />} {tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={DictAgendamentosOrganizados} />}
</div> </div>
</div> </div>
) )
@ -222,8 +277,65 @@ const Agendamento = () => {
</section> </section>
</div> </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> </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

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

View File

@ -171,7 +171,6 @@ function PatientCadastroManager( {setCurrentPage} ) {
}); });
setShowModal(true); setShowModal(true);
setTimeout(() => { setTimeout(() => {
setShowModal(false); setShowModal(false);
navigate('/secretaria/pacientes'); 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() const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização') console.log(authHeader)
var myHeaders = new Headers(); var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY); myHeaders.append("apikey", API_KEY);
@ -115,7 +115,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => response.json()) .then(response => response.json())
.then(result => setPacientes(result)) .then(result => {setPacientes(result); console.log(result)})
.catch(error => console.log('error', error)); .catch(error => console.log('error', error));
}, [isAuthenticated, getAuthorizationHeader]); }, [isAuthenticated, getAuthorizationHeader]);

View File

@ -118,7 +118,7 @@
border-radius: 10px; border-radius: 10px;
} }
#status-card-consulta-confirmado, .legenda-item-confirmado { #status-card-consulta-confirmado, .legenda-item-confirmed {
background-color: #eef8fb; background-color: #eef8fb;
border:3px solid #d8dfe7; border:3px solid #d8dfe7;
padding: 5px; padding: 5px;
@ -289,3 +289,72 @@ html[data-bs-theme="dark"] {
background-color: #005a9e; 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

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