modified: package-lock.json

modified:   package.json
modified:   src/App.jsx
modified:   src/index.css
modified:   src/pages/MedicalRecordsPage.jsx
modified:   src/pages/ReportsPage.jsx
modified:   src/repositories/medicalRecordRepository.js
This commit is contained in:
2026-05-09 23:32:04 -03:00
parent bcee06b908
commit fba021e048
7 changed files with 2125 additions and 303 deletions

654
package-lock.json generated
View File

@@ -8,6 +8,11 @@
"name": "projeto-residencia", "name": "projeto-residencia",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tiptap/extension-text-align": "^3.23.1",
"@tiptap/extension-underline": "^3.23.1",
"@tiptap/pm": "^3.23.1",
"@tiptap/react": "^3.23.1",
"@tiptap/starter-kit": "^3.23.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
@@ -459,6 +464,34 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT",
"optional": true
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1156,6 +1189,447 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8" "vite": "^5.2.0 || ^6 || ^7 || ^8"
} }
}, },
"node_modules/@tiptap/core": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.1.tgz",
"integrity": "sha512-8YvSGiJTeU5wPuGiYIIYgyiyaaT1CAx+kJL0bju0w871OvbJJj0T/ywhcmxGXW6pOal2T8X2xt9ZqE+vib0VJw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.23.1.tgz",
"integrity": "sha512-FdVZLZOkL06j3WLXOC2UeX7++Cj3qI2vfohruMJiz4vk1Q5UUH7G4+AykFzjzBJHrdEpkiRUkRpU1KZIWdbluw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.23.1.tgz",
"integrity": "sha512-EAYdNzyOjlQh2VBY1EhdxtiTjVMaOAD6P0ezms60dKRjd4oj/8grfXfUqwgo4NVdFb11Ks85vXoHuXJSylfR4A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.1.tgz",
"integrity": "sha512-1advMCpPkHD/3ucZhYmNau8B4tF0L6iRAFhUOglp5bBZDuq13+rYujh3cm4vFmjH9KqThzpcUDn+ZU2c+mTMyw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.1.tgz",
"integrity": "sha512-owWnBBI4t+jqVDY0naDjhsAmrNGldh4czouef2K+mEf032B7uGsDVCwKp1qaX1JZesyYDfvXOaIwT22hNID2mw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.1"
}
},
"node_modules/@tiptap/extension-code": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.23.1.tgz",
"integrity": "sha512-nGuhb4YghgTfkejwWHrD9GSpwcC5kkVmm2sN/UY4yceDw+PkyysYKJWZehRLTOC8GNgSAhq/EeQeq14Xwk6dyg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.23.1.tgz",
"integrity": "sha512-BdJGqM57CsKgYrQUZz78vIG8Yn7EpsE2pA7iKn5tYoSXpYtt0IaU4qB1heH7lwWD/vVCAm0YQVD7/0F+0++yhA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-document": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.23.1.tgz",
"integrity": "sha512-NA5Rx59HRwG6Hb6LwLpC5lE7z6vCj6f90S7RNNsnE+CyiXNR/OhY2BcjuxiGnascHvsnsAbvxGU3ymKMDgvDVg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.1.tgz",
"integrity": "sha512-WRN7e/h9m3uI5j9/+L6jcPhHbTL6aKxfFfQWZHNf5M8TqSL1P+/2h034td0XMj3n48i4fWyzjVUV9+sz6t2fDw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.1"
}
},
"node_modules/@tiptap/extension-floating-menu": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.1.tgz",
"integrity": "sha512-XrYHpLn1DpLFSGTko9F9xgbNamL6fGpWkK4wqgwPVbg/SJwQCDO/9p5D3DtJTwD+xgw4sQ9as4O6rt6jx8JT+Q==",
"license": "MIT",
"optional": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.1.tgz",
"integrity": "sha512-E4hB0xquUpEXy7kboLBazrFyRCsN0j0fsTFR8udgQf5xetAVPhOexSTKuzOcU/n0kxsKJin7laYYEag/Fd2KNw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.1"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.23.1.tgz",
"integrity": "sha512-XYkCKC5RVqMmmBk+nd22/6IDDx1OC54sdStH5VEHtfOrarriO0JztK8Mr0TijPPk9N4rKXsmndYZM2xyWZZytQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.1.tgz",
"integrity": "sha512-1z9yCSp8fevgX3r/4kWXO3of0WFCQWfYjWfHANvoJ4JQTYBkARjXlj1tbk5rrAJBFDDfKRkUpZOurXKgGo+h+g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.1.tgz",
"integrity": "sha512-30XUHXdEZxcz1FCWjz9HW2EEq06NQcAye6rXGnvHo6Y60iJ6MRsrX5byvceFNF9DTVtOIcUFBQ/psIiRcoi0KA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.23.1.tgz",
"integrity": "sha512-lZB9YCjoVNDoPMguya66nBvaS/2YpGN5iAcjAGx/JQkCAZeOAtl9+ALMzbWPKH6tQP6m98YtkY1T7RXr++T0bA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-link": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.23.1.tgz",
"integrity": "sha512-uOeyLqYQI0WG62agpFG24kVHSn3Z48gD8Y0uLLJbtzh/nDFC3d9So2sQGWlSVyMzsgkJ4k/9jNnxxsVO8qgJOg==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.3.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-list": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.1.tgz",
"integrity": "sha512-v1AeXPpagslgRZdOp7WdjCoO4TjjNP8RM2R6Gqx0/inGaNXnM8zCMshOxZlAb03Ad7kq/4RGJmkpM/Jjsi6dEQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.23.1.tgz",
"integrity": "sha512-Fk/884un5OSLCFxe2TbOmfp3sLMB5b76CnMjaSrvgfiaZnsV2WlJZGPXxCAPbxNIATTykNlSBsVuMBO7we64Vg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.1"
}
},
"node_modules/@tiptap/extension-list-keymap": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.1.tgz",
"integrity": "sha512-sHbE5sxiJzhgGn94GUAzD4qKM9SyImBrOlAGS/EIe+pausjqQE7xi+YW0gRo2jG+gXhSYl4/oAGXQXzmSInSUQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.1"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.1.tgz",
"integrity": "sha512-3GG7YFhVJWw/HWmRxvMMUC296x7TPBQRLsH4ryEC1SMAmVJnbTIvetyvIcLqLEXGW7Rj41S7SO8qjOXVceSOTA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.1"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.23.1.tgz",
"integrity": "sha512-GC7b6yAjASl1q9sNkPmukZmVYMfxx03EEhpMMrLYJY9GBz82Ald927yYQsOqf2aKA/Rjo/aZMYCGtjXkGk6aBA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.23.1.tgz",
"integrity": "sha512-+R5LG0ZW9SDZc4weA79uq6uUduVsCEph9tRcoQCRA82IVIiPYSTxTLew9odalmk/Mc7vdZvOK5jjtO5jUVw/rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-text": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.23.1.tgz",
"integrity": "sha512-k1Ki9bBV6mLz1mFP+Laqh1YHJ2MY0P8XzaMqpkgMndEBIJQ3XcpWQc5bfAlRnYcOI9ZXDbAgQ8CwgArxHmQWCQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-text-align": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.23.1.tgz",
"integrity": "sha512-ap4ZN31v57mVX2P+0OoW5iO+ehsUNe0C5MgF/Ta2F/HRmTCc1M1mFqYUCk8zJYX1TFRV18vqK2j6STRBk0R8ng==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extension-underline": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.1.tgz",
"integrity": "sha512-+PvHyVozHyxJ9oWCIQx5JHBZ7LAa/sFJUOFaKyfmel4gL9AbP52MmvrciXARlZHd1WCULJtdbLan0+x5/D/9hQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1"
}
},
"node_modules/@tiptap/extensions": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.1.tgz",
"integrity": "sha512-7UIn+idaVTVhdlP0KmgzBh8Csmwck357Dq4te5DuAxhSkN1gsXHlq39mpx907UYKJdSOgd+GMFeyOziPwSmbOQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.1"
}
},
"node_modules/@tiptap/pm": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.1.tgz",
"integrity": "sha512-8G+TkNsUHHAAJYREpA6fw+Dw/m2Y3Go4/QMQM8RYepid+wTeE1wSv7sBA/CBrphhYmJSWeTyCPtgQIxnTJXMCA==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.24.1",
"prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"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.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.23.1.tgz",
"integrity": "sha512-43zUwKOcsxRIcgiDbcEUagojhPIez2OIryaNG/uiDcRzkrUteiTu2wSJndkQqwouwh3wJEm+KOw8xybNYvU+qA==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"fast-equals": "^5.3.3",
"use-sync-external-store": "^1.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.23.1",
"@tiptap/extension-floating-menu": "^3.23.1"
},
"peerDependencies": {
"@tiptap/core": "3.23.1",
"@tiptap/pm": "3.23.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.23.1",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.23.1.tgz",
"integrity": "sha512-CURePHQagBaZIDJrHH3of4Nmi0VYGpZ6yBlkdFxFHBxY9aeG2/h5kn+oHo8GbzkSFsRV+9olzRgDTOULVgs8pQ==",
"license": "MIT",
"dependencies": {
"@tiptap/core": "^3.23.1",
"@tiptap/extension-blockquote": "^3.23.1",
"@tiptap/extension-bold": "^3.23.1",
"@tiptap/extension-bullet-list": "^3.23.1",
"@tiptap/extension-code": "^3.23.1",
"@tiptap/extension-code-block": "^3.23.1",
"@tiptap/extension-document": "^3.23.1",
"@tiptap/extension-dropcursor": "^3.23.1",
"@tiptap/extension-gapcursor": "^3.23.1",
"@tiptap/extension-hard-break": "^3.23.1",
"@tiptap/extension-heading": "^3.23.1",
"@tiptap/extension-horizontal-rule": "^3.23.1",
"@tiptap/extension-italic": "^3.23.1",
"@tiptap/extension-link": "^3.23.1",
"@tiptap/extension-list": "^3.23.1",
"@tiptap/extension-list-item": "^3.23.1",
"@tiptap/extension-list-keymap": "^3.23.1",
"@tiptap/extension-ordered-list": "^3.23.1",
"@tiptap/extension-paragraph": "^3.23.1",
"@tiptap/extension-strike": "^3.23.1",
"@tiptap/extension-text": "^3.23.1",
"@tiptap/extension-underline": "^3.23.1",
"@tiptap/extensions": "^3.23.1",
"@tiptap/pm": "^3.23.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1185,7 +1659,6 @@
"version": "19.2.14", "version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -1195,12 +1668,17 @@
"version": "19.2.3", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
@@ -1493,7 +1971,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": { "node_modules/date-fns": {
@@ -1776,6 +2253,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2399,6 +2885,12 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/linkifyjs": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
"license": "MIT"
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2513,6 +3005,12 @@
"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==",
"license": "MIT"
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -2644,6 +3142,135 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prosemirror-changeset": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
"license": "MIT",
"dependencies": {
"prosemirror-transform": "^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==",
"license": "MIT",
"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==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
"license": "MIT",
"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.5.0",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.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==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.4",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.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==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-state": "^1.4.4",
"prosemirror-transform": "^1.10.5",
"prosemirror-view": "^1.41.4"
}
},
"node_modules/prosemirror-transform": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.41.8",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2726,6 +3353,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"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==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -2901,6 +3534,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.10", "version": "8.0.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
@@ -2979,6 +3621,12 @@
} }
} }
}, },
"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==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -10,6 +10,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tiptap/extension-text-align": "^3.23.1",
"@tiptap/extension-underline": "^3.23.1",
"@tiptap/pm": "^3.23.1",
"@tiptap/react": "^3.23.1",
"@tiptap/starter-kit": "^3.23.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4" "react-dom": "^19.2.4"

View File

@@ -159,6 +159,23 @@ function resolveRoute(pathname, navigate, role) {
} }
} }
if (pathname === '/prontuario/novo') {
return {
element: <MedicalRecordsPage mode="new" navigate={navigate} />,
title: 'Novo prontuário',
withShell: true,
}
}
if (pathname.startsWith('/prontuario/')) {
const [, , recordId, action] = pathname.split('/')
return {
element: <MedicalRecordsPage mode={action === 'editar' ? 'edit' : 'detail'} navigate={navigate} recordId={recordId} />,
title: action === 'editar' ? 'Editar prontuário' : 'Prontuário',
withShell: true,
}
}
if (pathname.startsWith('/pacientes/')) { if (pathname.startsWith('/pacientes/')) {
const patientId = pathname.split('/')[2] const patientId = pathname.split('/')[2]
return { return {

View File

@@ -61,6 +61,14 @@ button:disabled {
color-scheme: light; color-scheme: light;
} }
[data-theme='light'] :where(input, textarea, [contenteditable='true'], .ProseMirror) {
caret-color: #000000;
}
[data-theme='light'] .auth-dark :where(input, textarea) {
caret-color: #e5e5e5;
}
[data-theme='light'] aside.bg-\[\#262626\] { [data-theme='light'] aside.bg-\[\#262626\] {
background-color: #f3f4f6; background-color: #f3f4f6;
} }
@@ -114,11 +122,21 @@ button:disabled {
border-color: #d6dee8; border-color: #d6dee8;
} }
[data-theme='light'] .divide-y.divide-\[\#404040\] > :not(:last-child),
[data-theme='light'] .divide-\[\#404040\] > :not(:last-child),
[data-theme='light'] table .divide-\[\#404040\] > tr:not(:last-child) {
border-color: #d6dee8;
}
[data-theme='light'] .border-\[\#525252\], [data-theme='light'] .border-\[\#525252\],
[data-theme='light'] .hover\:border-\[\#525252\]:hover { [data-theme='light'] .hover\:border-\[\#525252\]:hover {
border-color: #d1d5db; border-color: #d1d5db;
} }
[data-theme='light'] .border-\[\#5b4b75\] {
border-color: #d6dee8;
}
[data-theme='light'] .hover\:border-\[\#404040\]:hover, [data-theme='light'] .hover\:border-\[\#404040\]:hover,
[data-theme='light'] .disabled\:border-\[\#404040\]:disabled { [data-theme='light'] .disabled\:border-\[\#404040\]:disabled {
border-color: #d6dee8; border-color: #d6dee8;
@@ -302,6 +320,114 @@ button:disabled {
color: #9f1239; color: #9f1239;
} }
[data-theme='light'] .report-editor-backdrop {
background: rgba(15, 23, 42, 0.35);
}
[data-theme='light'] .report-editor-shell {
border-color: #c8d4e2;
background: #f8fbff;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
}
[data-theme='light'] .report-editor-header,
[data-theme='light'] .report-editor-footer {
border-color: #c8d4e2;
background: #ffffff;
}
[data-theme='light'] .report-editor-sidebar {
border-color: #c8d4e2;
background: #edf4fb;
}
[data-theme='light'] .report-editor-body {
background: #f8fbff;
}
[data-theme='light'] .report-template-trigger,
[data-theme='light'] .report-template-menu,
[data-theme='light'] .report-rich-editor,
[data-theme='light'] .report-rich-toolbar {
border-color: #c8d4e2;
background: #ffffff;
color: #333333;
}
[data-theme='light'] .report-rich-surface {
background: #ffffff;
caret-color: #000000;
color: #333333;
}
[data-theme='light'] .report-rich-toolbar select {
border-color: #c8d4e2;
background: #ffffff;
color: #333333;
}
[data-theme='light'] .report-rich-toolbar button {
color: #4b5563;
}
[data-theme='light'] .report-rich-toolbar button[aria-pressed='true'],
[data-theme='light'] .report-rich-toolbar button:hover {
color: #2563eb;
}
.report-rich-surface {
caret-color: #e5e5e5;
cursor: text;
min-height: 560px;
}
.report-rich-surface * {
cursor: text;
}
.report-rich-surface.ProseMirror-focused {
caret-color: #e5e5e5;
}
[data-theme='light'] .report-rich-surface.ProseMirror-focused {
caret-color: #000000;
}
.report-rich-surface p {
margin: 0 0 0.75rem;
}
.report-rich-surface h2 {
margin: 0 0 0.85rem;
font-size: 1.1rem;
font-weight: 700;
}
.report-rich-surface h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
font-weight: 700;
}
.report-rich-surface ul,
.report-rich-surface ol {
margin: 0.5rem 0 0.75rem;
padding-left: 1.4rem;
}
.report-rich-surface .is-empty::before {
color: #737373;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
[data-theme='light'] .report-rich-toolbar button:hover,
[data-theme='light'] .report-template-menu button:hover {
background: #e8edf4;
}
.agenda-calendar-shell { .agenda-calendar-shell {
border-color: #3b3b3b; border-color: #3b3b3b;
background: #202020; background: #202020;

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import TextAlign from '@tiptap/extension-text-align'
import { normalizeRole } from '../config/permissions.js' import { normalizeRole } from '../config/permissions.js'
import { patientRepository } from '../repositories/patientRepository.js' import { patientRepository } from '../repositories/patientRepository.js'
import { professionalRepository } from '../repositories/professionalRepository.js' import { professionalRepository } from '../repositories/professionalRepository.js'
import { profileRepository } from '../repositories/profileRepository.js' import { profileRepository } from '../repositories/profileRepository.js'
import { reportRepository } from '../repositories/reportRepository.js' import { reportRepository } from '../repositories/reportRepository.js'
import { StethoscopeIcon } from '../components/Brand.jsx'
const ITEMS_PER_PAGE = 25 const ITEMS_PER_PAGE = 25
@@ -349,18 +354,25 @@ export function ReportsPage({ role }) {
setSaving(true) setSaving(true)
const plainContent = stripHtml(editor.contentHtml)
const fallbackAuthor =
currentProfessional?.name ||
viewerProfile?.name ||
viewerProfile?.email ||
'Profissional MediConnect'
const payload = { const payload = {
orderNumber: editor.id ? editor.orderNumber : `REL-${Date.now()}`, orderNumber: editor.id ? editor.orderNumber : `REL-${Date.now()}`,
patientId: editor.patientId, patientId: editor.patientId || patientOptions[0]?.id || '',
status: editor.status, status: editor.status,
exam: editor.exam, exam: editor.exam || 'Relatório médico',
requestedBy: editor.requestedBy, requestedBy: editor.requestedBy || fallbackAuthor,
cidCode: editor.cidCode, cidCode: editor.cidCode || 'Z00.0',
diagnosis: editor.diagnosis, diagnosis: editor.diagnosis || plainContent.slice(0, 240) || 'Relatório médico registrado em prontuário.',
conclusion: editor.conclusion, conclusion: editor.conclusion || plainContent.slice(0, 240) || 'Relatório médico salvo no sistema.',
contentHtml: editor.contentHtml, contentHtml: editor.contentHtml,
contentJson: editor.contentJson, contentJson: editor.contentJson,
dueAt: editor.dueAt ? new Date(editor.dueAt).toISOString() : '', dueAt: editor.dueAt ? new Date(editor.dueAt).toISOString() : new Date().toISOString(),
createdBy: editor.id ? undefined : viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined, createdBy: editor.id ? undefined : viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined,
updatedBy: viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined, updatedBy: viewerProfile?.id || currentProfessional?.userId || currentProfessional?.id || undefined,
} }
@@ -557,7 +569,7 @@ export function ReportsPage({ role }) {
</section> </section>
{editorOpen ? ( {editorOpen ? (
<ReportEditorModalV2 <ReportEditorModalV3
editor={editor} editor={editor}
onChange={setEditor} onChange={setEditor}
onClose={() => setEditorOpen(false)} onClose={() => setEditorOpen(false)}
@@ -605,8 +617,185 @@ function ReportRow({ onEdit, onView, report }) {
) )
} }
function ReportEditorModalV3({ editor, onChange, onClose, onSave, saving }) {
const [templateCategory, setTemplateCategory] = useState('Todos')
const [templateSearch, setTemplateSearch] = useState('')
const [templatesOpen, setTemplatesOpen] = useState(false)
const [categoriesOpen, setCategoriesOpen] = useState(true)
const isValid = isReportEditorValid(editor)
const filteredTemplates = reportTemplates.filter((template) => {
const matchesCategory = templateCategory === 'Todos' || template.category === templateCategory
const query = normalizeSearch(templateSearch)
const matchesSearch = !query || normalizeSearch([template.title, template.description, template.tags.join(' ')].join(' ')).includes(query)
return matchesCategory && matchesSearch
})
function updateField(field, value) {
onChange((current) => ({ ...current, [field]: value }))
}
function applyTemplate(template) {
setTemplatesOpen(false)
onChange((current) => ({
...current,
exam: current.exam || template.exam,
cidCode: current.cidCode || template.cidCode,
diagnosis: current.diagnosis || template.diagnosis,
conclusion: current.conclusion || template.conclusion,
contentHtml: current.contentHtml ? `${current.contentHtml}<hr>${template.contentHtml}` : template.contentHtml,
contentJson: {
templateId: template.id,
templateTitle: template.title,
appliedAt: new Date().toISOString(),
},
}))
}
return (
<div className="report-editor-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-3" onClick={onClose}>
<div
className="report-editor-shell flex max-h-[94vh] w-full max-w-6xl flex-col overflow-hidden rounded-xl border border-[#404040] bg-[#242424] shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="report-editor-header flex items-center justify-between border-b border-[#404040] px-6 py-4">
<div className="flex items-center gap-3">
<span className="grid size-9 place-items-center rounded-sm bg-[#3b82f6] text-white">
<StethoscopeIcon className="size-5" />
</span>
<div>
<h2 className="text-lg font-bold text-[#f5f5f5]">{editor.id ? 'Editar relatório' : 'Novo relatório'}</h2>
<p className="text-xs text-[#a3a3a3]">Escolha um template opcional e edite o conteúdo do relatório.</p>
</div>
</div>
<button className="grid size-9 place-items-center rounded-sm text-[#a3a3a3] transition hover:bg-[#303030] hover:text-[#e5e5e5]" onClick={onClose} type="button">
<ReportIcon className="size-4" name="x" />
</button>
</div>
<div className={`grid min-h-0 flex-1 ${categoriesOpen ? 'lg:grid-cols-[230px_minmax(0,1fr)]' : 'lg:grid-cols-[56px_minmax(0,1fr)]'}`}>
<aside className="report-editor-sidebar min-h-0 border-b border-[#404040] bg-[#202020] p-3 lg:border-b-0 lg:border-r">
<button
className="mb-3 flex h-9 w-full items-center justify-between rounded-sm border border-[#404040] bg-[#171717] px-2 text-xs font-bold uppercase tracking-[0.12em] text-[#a3a3a3] transition hover:bg-[#303030] hover:text-[#e5e5e5]"
onClick={() => setCategoriesOpen((current) => !current)}
type="button"
>
{categoriesOpen ? <span>Categorias</span> : null}
<ReportIcon className="size-4" name={categoriesOpen ? 'chevron-left' : 'chevron-right'} />
</button>
{categoriesOpen ? (
<div className="space-y-1">
{templateCategories.map((category) => {
const count = category === 'Todos' ? reportTemplates.length : reportTemplates.filter((template) => template.category === category).length
return (
<button
className={`flex w-full items-center justify-between rounded-sm px-3 py-2 text-left text-sm font-semibold transition ${
templateCategory === category
? 'bg-[#3b82f6]/15 text-[#3b82f6]'
: 'text-[#a3a3a3] hover:bg-[#303030] hover:text-[#e5e5e5]'
}`}
key={category}
onClick={() => setTemplateCategory(category)}
type="button"
>
<span>{category}</span>
<span className="rounded-full bg-black/20 px-2 py-0.5 text-[10px]">{count}</span>
</button>
)
})}
</div>
) : null}
</aside>
<main className="report-editor-body min-h-0 overflow-y-auto p-5">
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<DarkField label="Status *">
<select className={`${inputClass} md:w-52`} onChange={(event) => updateField('status', event.target.value)} value={editor.status}>
<option value="draft">Rascunho</option>
<option value="finalized">Finalizado</option>
</select>
</DarkField>
<div className="relative">
<button
className="report-template-trigger inline-flex h-10 items-center gap-2 rounded-sm border border-[#404040] bg-[#171717] px-4 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]"
onClick={() => setTemplatesOpen((current) => !current)}
type="button"
>
<ReportIcon className="size-4" name="file" />
Templates
<ReportIcon className="size-4" name="chevron-right" />
</button>
{templatesOpen ? (
<div className="report-template-menu absolute right-0 top-12 z-10 w-[min(28rem,calc(100vw-2rem))] rounded-md border border-[#404040] bg-[#202020] p-3 shadow-2xl">
<div className="relative mb-3">
<ReportIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[#a3a3a3]" name="search" />
<input
className="h-10 w-full rounded-sm border border-[#404040] bg-[#171717] pl-10 pr-3 text-sm text-[#e5e5e5] outline-none transition placeholder:text-[#a3a3a3] focus:border-[#3b82f6]"
onChange={(event) => setTemplateSearch(event.target.value)}
placeholder="Buscar templates..."
value={templateSearch}
/>
</div>
<div className="max-h-80 overflow-y-auto">
{filteredTemplates.length ? (
filteredTemplates.map((template) => (
<button
className="block w-full rounded-sm border border-transparent px-3 py-3 text-left transition hover:border-[#3b82f6]/40 hover:bg-[#303030]"
key={template.id}
onClick={() => applyTemplate(template)}
type="button"
>
<span className="flex items-center justify-between gap-3">
<span className="font-semibold text-[#f5f5f5]">{template.title}</span>
{template.popular ? <span className="rounded bg-amber-500/15 px-2 py-0.5 text-[10px] font-bold text-amber-300">Popular</span> : null}
</span>
<span className="mt-1 block text-xs leading-5 text-[#a3a3a3]">{template.description}</span>
</button>
))
) : (
<p className="px-3 py-4 text-sm text-[#a3a3a3]">Nenhum template encontrado.</p>
)}
</div>
</div>
) : null}
</div>
</div>
<DarkField label="Editor de texto">
<RichTextEditor
onChange={(value) => updateField('contentHtml', value)}
value={editor.contentHtml}
/>
</DarkField>
</main>
</div>
<div className="report-editor-footer flex flex-wrap items-center justify-between gap-3 border-t border-[#404040] px-6 py-4">
<p className="text-xs font-semibold text-amber-300">
{!isValid ? '* Preencha o editor de texto para salvar.' : 'Relatório pronto para salvar.'}
</p>
<div className="flex gap-3">
<button className="rounded-sm border border-[#404040] bg-[#262626] px-4 py-2 text-sm font-semibold text-[#e5e5e5] transition hover:bg-[#303030]" onClick={onClose} type="button">
Cancelar
</button>
<button
className="inline-flex items-center gap-2 rounded-sm border border-[#3b82f6] bg-[#3b82f6] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#2563eb] disabled:cursor-not-allowed disabled:border-[#404040] disabled:bg-[#303030] disabled:text-[#737373]"
disabled={!isValid || saving}
onClick={onSave}
type="button"
>
<ReportIcon className="size-3.5" name="save" />
{saving ? 'Salvando...' : editor.status === 'finalized' ? 'Liberar relatório' : 'Salvar rascunho'}
</button>
</div>
</div>
</div>
</div>
)
}
function ReportEditorModalV2({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) { function ReportEditorModalV2({ editor, onChange, onClose, onSave, patientOptions, professionalOptions, saving }) {
const editorRef = useRef(null)
const [requesterSearch, setRequesterSearch] = useState(editor.requestedBy || '') const [requesterSearch, setRequesterSearch] = useState(editor.requestedBy || '')
const [patientSearch, setPatientSearch] = useState('') const [patientSearch, setPatientSearch] = useState('')
const [templateSearch, setTemplateSearch] = useState('') const [templateSearch, setTemplateSearch] = useState('')
@@ -651,21 +840,6 @@ function ReportEditorModalV2({ editor, onChange, onClose, onSave, patientOptions
})) }))
} }
function runCommand(command, value = null) {
editorRef.current?.focus()
document.execCommand(command, false, value)
updateField('contentHtml', editorRef.current?.innerHTML || '')
}
function insertToken(token) {
const values = {
patient: selectedPatient?.name || '[Paciente]',
date: new Date().toLocaleDateString('pt-BR'),
doctor: editor.requestedBy || '[Médico]',
}
runCommand('insertText', values[token] || '')
}
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-3" onClick={onClose}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-3" onClick={onClose}>
<div <div
@@ -851,10 +1025,7 @@ function ReportEditorModalV2({ editor, onChange, onClose, onSave, patientOptions
<DarkField label="Conteúdo"> <DarkField label="Conteúdo">
<RichTextEditor <RichTextEditor
editorRef={editorRef}
onChange={(value) => updateField('contentHtml', value)} onChange={(value) => updateField('contentHtml', value)}
onCommand={runCommand}
onInsertToken={insertToken}
value={editor.contentHtml} value={editor.contentHtml}
/> />
</DarkField> </DarkField>
@@ -1104,60 +1275,135 @@ function SearchPickList({ emptyText, items, labelKey, onSelect, selectedValue, v
) )
} }
function RichTextEditor({ editorRef, onChange, onCommand, onInsertToken, value }) { function RichTextEditor({ onChange, value }) {
const lastSyncedHtmlRef = useRef(value || '')
const applyingExternalContentRef = useRef(false)
const tiptapEditor = useEditor({
extensions: [
StarterKit,
Underline,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
],
content: value || '',
editorProps: {
attributes: {
class: 'report-rich-surface min-h-[560px] px-4 py-3 text-sm leading-6 text-[#e5e5e5] outline-none',
},
},
shouldRerenderOnTransaction: false,
onUpdate: ({ editor: currentEditor }) => {
if (applyingExternalContentRef.current) return
const nextHtml = currentEditor.getHTML()
lastSyncedHtmlRef.current = nextHtml
onChange(nextHtml)
},
})
useEffect(() => {
if (!tiptapEditor) return
const nextValue = value || ''
if (lastSyncedHtmlRef.current === nextValue) return
if (tiptapEditor.getHTML() === nextValue) {
lastSyncedHtmlRef.current = nextValue
return
}
applyingExternalContentRef.current = true
try {
tiptapEditor.commands.setContent(nextValue, { emitUpdate: false })
} finally {
applyingExternalContentRef.current = false
}
lastSyncedHtmlRef.current = nextValue
}, [tiptapEditor, value])
function insertToken(token) {
const values = {
patient: '[Paciente]',
date: new Date().toLocaleDateString('pt-BR'),
doctor: '[Medico]',
}
tiptapEditor?.chain().focus().insertContent(values[token] || '').run()
}
const blockFormat = tiptapEditor?.isActive('heading', { level: 2 })
? 'h2'
: tiptapEditor?.isActive('heading', { level: 3 })
? 'h3'
: 'p'
return ( return (
<div className="overflow-hidden rounded-sm border border-[#404040] bg-[#171717]"> <div className="report-rich-editor overflow-hidden rounded-sm border border-[#404040] bg-[#171717]">
<div className="flex flex-wrap items-center gap-1 border-b border-[#404040] bg-[#202020] px-3 py-2"> <div className="report-rich-toolbar flex flex-wrap items-center gap-1 border-b border-[#404040] bg-[#202020] px-3 py-2">
<ToolbarButton label="Desfazer" name="undo" onClick={() => onCommand('undo')} /> <TipTapToolbarButton disabled={!tiptapEditor?.can().undo()} label="Desfazer" name="undo" onClick={() => tiptapEditor?.chain().focus().undo().run()} />
<ToolbarButton label="Refazer" name="redo" onClick={() => onCommand('redo')} /> <TipTapToolbarButton disabled={!tiptapEditor?.can().redo()} label="Refazer" name="redo" onClick={() => tiptapEditor?.chain().focus().redo().run()} />
<span className="mx-1 h-5 w-px bg-[#404040]" /> <span className="mx-1 h-5 w-px bg-[#404040]" />
<select className="h-8 rounded-sm border border-[#404040] bg-[#171717] px-2 text-xs font-semibold text-[#d4d4d4]" onChange={(event) => onCommand('formatBlock', event.target.value)} defaultValue="p"> <select
<option value="p">Padrão</option> className="h-8 rounded-sm border border-[#404040] bg-[#171717] px-2 text-xs font-semibold text-[#d4d4d4]"
<option value="h2">Título</option> onChange={(event) => {
<option value="h3">Subtítulo</option> const selected = event.target.value
if (selected === 'h2') {
tiptapEditor?.chain().focus().toggleHeading({ level: 2 }).run()
return
}
if (selected === 'h3') {
tiptapEditor?.chain().focus().toggleHeading({ level: 3 }).run()
return
}
tiptapEditor?.chain().focus().setParagraph().run()
}}
value={blockFormat}
>
<option value="p">Padrao</option>
<option value="h2">Titulo</option>
<option value="h3">Subtitulo</option>
</select> </select>
<ToolbarButton active label="Negrito" name="bold" onClick={() => onCommand('bold')} /> <TipTapToolbarButton active={tiptapEditor?.isActive('bold')} label="Negrito" name="bold" onClick={() => tiptapEditor?.chain().focus().toggleBold().run()} />
<ToolbarButton label="Itálico" name="italic" onClick={() => onCommand('italic')} /> <TipTapToolbarButton active={tiptapEditor?.isActive('italic')} label="Italico" name="italic" onClick={() => tiptapEditor?.chain().focus().toggleItalic().run()} />
<ToolbarButton label="Sublinhado" name="underline" onClick={() => onCommand('underline')} /> <TipTapToolbarButton active={tiptapEditor?.isActive('underline')} label="Sublinhado" name="underline" onClick={() => tiptapEditor?.chain().focus().toggleUnderline().run()} />
<ToolbarButton label="Tachado" name="strike" onClick={() => onCommand('strikeThrough')} /> <TipTapToolbarButton active={tiptapEditor?.isActive('strike')} label="Tachado" name="strike" onClick={() => tiptapEditor?.chain().focus().toggleStrike().run()} />
<span className="mx-1 h-5 w-px bg-[#404040]" /> <span className="mx-1 h-5 w-px bg-[#404040]" />
<ToolbarButton label="Alinhar à esquerda" name="align-left" onClick={() => onCommand('justifyLeft')} /> <TipTapToolbarButton active={tiptapEditor?.isActive({ textAlign: 'left' })} label="Alinhar a esquerda" name="align-left" onClick={() => tiptapEditor?.chain().focus().setTextAlign('left').run()} />
<ToolbarButton label="Centralizar" name="align-center" onClick={() => onCommand('justifyCenter')} /> <TipTapToolbarButton active={tiptapEditor?.isActive({ textAlign: 'center' })} label="Centralizar" name="align-center" onClick={() => tiptapEditor?.chain().focus().setTextAlign('center').run()} />
<ToolbarButton label="Alinhar à direita" name="align-right" onClick={() => onCommand('justifyRight')} /> <TipTapToolbarButton active={tiptapEditor?.isActive({ textAlign: 'right' })} label="Alinhar a direita" name="align-right" onClick={() => tiptapEditor?.chain().focus().setTextAlign('right').run()} />
<ToolbarButton label="Lista" name="list" onClick={() => onCommand('insertUnorderedList')} /> <TipTapToolbarButton active={tiptapEditor?.isActive('bulletList')} label="Lista" name="list" onClick={() => tiptapEditor?.chain().focus().toggleBulletList().run()} />
<div className="ml-auto flex items-center gap-1"> <div className="ml-auto flex items-center gap-1">
<span className="mr-1 text-[11px] text-[#a3a3a3]">Inserir:</span> <span className="mr-1 text-[11px] text-[#a3a3a3]">Inserir:</span>
<button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => onInsertToken('patient')} type="button"> <button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => insertToken('patient')} type="button">
+ Paciente + Paciente
</button> </button>
<button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => onInsertToken('date')} type="button"> <button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => insertToken('date')} type="button">
+ Data + Data
</button> </button>
<button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => onInsertToken('doctor')} type="button"> <button className="h-8 rounded-sm border border-[#3b82f6]/40 px-2 text-xs font-semibold text-[#3b82f6] hover:bg-[#3b82f6]/10" onClick={() => insertToken('doctor')} type="button">
+ Médico + Medico
</button> </button>
</div> </div>
</div> </div>
<div <EditorContent editor={tiptapEditor} />
className="min-h-[320px] px-4 py-3 text-sm leading-6 text-[#e5e5e5] outline-none empty:before:text-[#737373]"
contentEditable
dangerouslySetInnerHTML={{ __html: value || '' }}
onInput={(event) => onChange(event.currentTarget.innerHTML)}
ref={editorRef}
role="textbox"
suppressContentEditableWarning
/>
</div> </div>
) )
} }
function ToolbarButton({ active = false, label, name, onClick }) { function TipTapToolbarButton({ active = false, disabled = false, label, name, onClick }) {
return ( return (
<button <button
aria-label={label} aria-label={label}
aria-pressed={active}
className={`grid size-8 place-items-center rounded-sm transition ${ className={`grid size-8 place-items-center rounded-sm transition ${
active ? 'bg-[#3b82f6]/20 text-[#3b82f6]' : 'text-[#a3a3a3] hover:bg-[#303030] hover:text-[#e5e5e5]' active ? 'bg-[#3b82f6]/20 text-[#3b82f6]' : 'text-[#a3a3a3] hover:bg-[#303030] hover:text-[#e5e5e5]'
}`} } disabled:cursor-not-allowed disabled:opacity-40`}
disabled={disabled}
onMouseDown={(event) => event.preventDefault()}
onClick={onClick} onClick={onClick}
title={label} title={label}
type="button" type="button"
@@ -1249,10 +1495,10 @@ function FilterField({ children, label }) {
function DarkField({ children, label }) { function DarkField({ children, label }) {
return ( return (
<label className="block"> <div className="block">
<span className={labelClass}>{label}</span> <span className={labelClass}>{label}</span>
{children} {children}
</label> </div>
) )
} }
@@ -1340,17 +1586,19 @@ function uniqueValues(values) {
function isReportEditorValid(editor) { function isReportEditorValid(editor) {
return [ return [
editor.patientId,
editor.status, editor.status,
editor.exam, stripHtml(editor.contentHtml),
editor.requestedBy,
editor.cidCode,
editor.diagnosis,
editor.conclusion,
editor.dueAt,
].every((value) => String(value || '').trim()) ].every((value) => String(value || '').trim())
} }
function stripHtml(value) {
return String(value || '')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
function normalizeSearch(value) { function normalizeSearch(value) {
return String(value || '') return String(value || '')
.normalize('NFD') .normalize('NFD')

View File

@@ -1,15 +1,308 @@
const STORAGE_KEY = 'mediconnect.medicalRecords.v2'
const INITIAL_RECORDS = [
{
id: 'record-1',
patientId: 'mock-carlos-eduardo',
patient: 'Carlos Eduardo Santos',
patientDocument: 'CPF nao informado',
patientEmail: 'carlos.santos@example.com',
patientPhone: '11999990001',
dateTime: '2026-03-27T10:30',
createdAt: '2026-03-27T10:30:00.000Z',
updatedAt: '2026-03-27T10:30:00.000Z',
doctor: 'Dra. Ana Silva',
type: 'Consulta Retorno',
cid: 'I10 - Hipertensao',
status: 'completo',
summary: 'Paciente relata melhora com medicacao. PA: 130/85. Mantida conduta.',
diagnosticReasoning: 'Quadro compativel com hipertensao arterial sistemica em acompanhamento, com melhora apos adesao medicamentosa.',
diagnosticHypotheses: 'HAS primaria; efeito de baixa adesao previa ao tratamento; risco cardiovascular global moderado.',
definitiveDiagnosis: 'Hipertensao arterial sistemica controlada.',
prescriptions: 'Manter losartana 50 mg de 12/12h e hidroclorotiazida 25 mg pela manha.',
procedures: 'Afericao pressorica seriada e orientacao sobre automonitoramento domiciliar.',
surgeries: 'Nao se aplica no atendimento atual.',
orientations: 'Reduzir sodio, manter atividade fisica regular e retornar com diario pressorico.',
labResults: 'Exames laboratoriais sem alteracoes relevantes no periodo.',
imageResults: 'Sem exames de imagem novos para este atendimento.',
multiprofessionalNotes: 'Enfermagem orientou tecnica correta de afericao de pressao arterial.',
signature: 'Dra. Ana Silva - CRM 123456',
professionalStamp: 'Assinado digitalmente por Dra. Ana Silva em 27/03/2026 10:30',
},
{
id: 'record-2',
patientId: 'mock-mariana-costa',
patient: 'Mariana Costa',
patientDocument: 'CPF nao informado',
patientEmail: 'mariana.costa@example.com',
patientPhone: '11999990002',
dateTime: '2026-03-26T15:00',
createdAt: '2026-03-26T15:00:00.000Z',
updatedAt: '2026-03-26T15:00:00.000Z',
doctor: 'Dra. Ana Silva',
type: 'Exame',
cid: 'Z01.7 - Exame laboratorial',
status: 'completo',
summary: 'Resultados de hemograma dentro da normalidade. Solicitar retorno em 6 meses.',
diagnosticReasoning: 'Resultados laboratoriais analisados em conjunto com quadro clinico estavel.',
diagnosticHypotheses: 'Acompanhamento preventivo sem sinais laboratoriais de alarme.',
definitiveDiagnosis: 'Exame laboratorial sem alteracoes clinicamente significativas.',
prescriptions: 'Sem nova prescricao medicamentosa.',
procedures: 'Revisao de exames laboratoriais e comparacao com historico previo.',
surgeries: 'Nao se aplica.',
orientations: 'Manter rotina preventiva e retorno em 6 meses ou antes se houver sintomas.',
labResults: 'Hemograma completo dentro dos parametros de referencia.',
imageResults: 'Sem exames de imagem relacionados.',
multiprofessionalNotes: 'Equipe administrativa orientou retirada de copia dos exames.',
signature: 'Dra. Ana Silva - CRM 123456',
professionalStamp: 'Assinado digitalmente por Dra. Ana Silva em 26/03/2026 15:00',
},
{
id: 'record-3',
patientId: 'mock-joao-pedro',
patient: 'Joao Pedro Alves',
patientDocument: 'CPF nao informado',
patientEmail: 'joao.alves@example.com',
patientPhone: '11999990003',
dateTime: '2026-03-25T09:15',
createdAt: '2026-03-25T09:15:00.000Z',
updatedAt: '2026-03-25T09:15:00.000Z',
doctor: 'Dr. Carlos Mendes',
type: 'Primeira Consulta',
cid: 'R10 - Dor abdominal',
status: 'rascunho',
summary: 'Queixa de dor abdominal ha 2 semanas. Solicitados exames complementares.',
diagnosticReasoning: 'Dor abdominal subaguda, sem sinais de peritonite, em investigacao etiologica.',
diagnosticHypotheses: 'Dispepsia funcional; gastrite; doenca biliar; sindrome do intestino irritavel.',
definitiveDiagnosis: 'Diagnostico definitivo pendente de exames complementares.',
prescriptions: 'Sintomatico conforme dor e orientacao de retorno se piora.',
procedures: 'Exame fisico abdominal e solicitacao de exames complementares.',
surgeries: 'Nao indicada ate o momento.',
orientations: 'Retornar com exames, procurar urgencia se febre, vomitos persistentes ou dor intensa.',
labResults: 'Hemograma, PCR e funcao hepatica solicitados.',
imageResults: 'Ultrassonografia abdominal solicitada.',
multiprofessionalNotes: 'Nutricionista podera ser acionada conforme resultado dos exames.',
signature: 'Dr. Carlos Mendes - CRM 654321',
professionalStamp: 'Rascunho criado por Dr. Carlos Mendes em 25/03/2026 09:15',
},
{
id: 'record-4',
patientId: 'mock-fernanda-lima',
patient: 'Fernanda Lima',
patientDocument: 'CPF nao informado',
patientEmail: 'fernanda.lima@example.com',
patientPhone: '11999990004',
dateTime: '2026-03-24T11:00',
createdAt: '2026-03-24T11:00:00.000Z',
updatedAt: '2026-03-24T11:00:00.000Z',
doctor: 'Dra. Ana Silva',
type: 'Avaliacao Pre-Op',
cid: 'K80 - Colelitiase',
status: 'completo',
summary: 'Apta para procedimento cirurgico. Exames pre-operatorios normais.',
diagnosticReasoning: 'Colelitiase sintomatica com avaliacao clinica favoravel para procedimento proposto.',
diagnosticHypotheses: 'Colelitiase sintomatica; baixo risco cardiopulmonar para cirurgia eletiva.',
definitiveDiagnosis: 'Colelitiase com indicacao de abordagem cirurgica eletiva.',
prescriptions: 'Manter medicacoes habituais conforme orientacao anestesica.',
procedures: 'Avaliacao pre-operatoria e revisao de exames.',
surgeries: 'Colecistectomia videolaparoscopica proposta pela equipe cirurgica.',
orientations: 'Jejum e orientacoes pre-operatorias conforme protocolo institucional.',
labResults: 'Hemograma, coagulograma e funcao renal sem contraindicacoes.',
imageResults: 'Ultrassonografia com colelitíase, sem sinais de colecistite aguda.',
multiprofessionalNotes: 'Anestesia orientou avaliacao pre-anestesica complementar.',
signature: 'Dra. Ana Silva - CRM 123456',
professionalStamp: 'Assinado digitalmente por Dra. Ana Silva em 24/03/2026 11:00',
},
{
id: 'record-5',
patientId: 'mock-roberto-campos',
patient: 'Roberto Campos',
patientDocument: 'CPF nao informado',
patientEmail: 'roberto.campos@example.com',
patientPhone: '11999990005',
dateTime: '2026-03-22T16:20',
createdAt: '2026-03-22T16:20:00.000Z',
updatedAt: '2026-03-22T16:20:00.000Z',
doctor: 'Dr. Roberto Nunes',
type: 'Consulta Retorno',
cid: 'E11 - DM Tipo 2',
status: 'completo',
summary: 'HbA1c: 7.2%. Ajuste de metformina. Retorno em 3 meses.',
diagnosticReasoning: 'Diabetes mellitus tipo 2 com controle parcial, necessitando ajuste terapeutico.',
diagnosticHypotheses: 'DM2 em controle parcial; risco metabolico associado.',
definitiveDiagnosis: 'Diabetes mellitus tipo 2.',
prescriptions: 'Ajuste de metformina conforme tolerancia e manutencao de medidas nao farmacologicas.',
procedures: 'Revisao de exames metabolicos e avaliacao de adesao.',
surgeries: 'Nao se aplica.',
orientations: 'Dieta, atividade fisica, monitoramento glicemico e retorno em 3 meses.',
labResults: 'HbA1c 7,2%; demais exames revisados em consulta.',
imageResults: 'Sem exames de imagem novos.',
multiprofessionalNotes: 'Encaminhado para orientacao nutricional.',
signature: 'Dr. Roberto Nunes - CRM 778899',
professionalStamp: 'Assinado digitalmente por Dr. Roberto Nunes em 22/03/2026 16:20',
},
]
export const medicalRecordRepository = { export const medicalRecordRepository = {
getRecordTypes() { getRecordTypes() {
return ['Consulta Retorno', 'Primeira Consulta', 'Exame', 'Avaliacao Pre-Op'] return ['Consulta Retorno', 'Primeira Consulta', 'Exame', 'Avaliacao Pre-Op', 'Evolucao Clinica', 'Registro Multiprofissional']
}, },
getInitialRecords() { getInitialRecords() {
return readRecords()
},
getAll() {
return readRecords()
},
getById(recordId) {
return readRecords().find((record) => String(record.id) === String(recordId)) || null
},
create(data) {
const records = readRecords()
const now = new Date().toISOString()
const record = normalizeRecord({
...data,
id: data.id || `record-${Date.now()}`,
createdAt: data.createdAt || now,
updatedAt: now,
})
writeRecords([record, ...records])
return record
},
update(recordId, data) {
const records = readRecords()
const now = new Date().toISOString()
let updatedRecord = null
const nextRecords = records.map((record) => {
if (String(record.id) !== String(recordId)) return record
updatedRecord = normalizeRecord({ ...record, ...data, id: record.id, updatedAt: now })
return updatedRecord
})
writeRecords(nextRecords)
return updatedRecord
},
getMockReportHistory(patientId, patientName) {
const baseName = patientName || 'Paciente'
return [ return [
{ id: 'record-1', patient: 'Carlos Eduardo Santos', date: '27/03/2026', doctor: 'Dra. Ana Silva', type: 'Consulta Retorno', cid: 'I10 - Hipertensao', status: 'completo', summary: 'Paciente relata melhora com medicacao. PA: 130/85. Mantida conduta.' }, {
{ id: 'record-2', patient: 'Mariana Costa', date: '26/03/2026', doctor: 'Dra. Ana Silva', type: 'Exame', cid: 'Z01.7 - Exame laboratorial', status: 'completo', summary: 'Resultados de hemograma dentro da normalidade. Solicitar retorno em 6 meses.' }, id: `${patientId || 'mock'}-report-1`,
{ id: 'record-3', patient: 'Joao Pedro Alves', date: '25/03/2026', doctor: 'Dr. Carlos Mendes', type: 'Primeira Consulta', cid: 'R10 - Dor abdominal', status: 'rascunho', summary: 'Queixa de dor abdominal ha 2 semanas. Solicitados exames complementares.' }, title: 'Relatorio de consulta medica',
{ id: 'record-4', patient: 'Fernanda Lima', date: '24/03/2026', doctor: 'Dra. Ana Silva', type: 'Avaliacao Pre-Op', cid: 'K80 - Colelitiase', status: 'completo', summary: 'Apta para procedimento cirurgico. Exames pre-operatorios normais.' }, status: 'Finalizado',
{ id: 'record-5', patient: 'Roberto Campos', date: '22/03/2026', doctor: 'Dr. Roberto Nunes', type: 'Consulta Retorno', cid: 'E11 - DM Tipo 2', status: 'completo', summary: 'HbA1c: 7.2%. Ajuste de metformina. Retorno em 3 meses.' }, createdAt: '2026-03-27T13:30:00.000Z',
author: 'Dra. Ana Silva',
summary: `Resumo clinico recente de ${baseName}, com conduta registrada em prontuario.`,
},
{
id: `${patientId || 'mock'}-report-2`,
title: 'Laudo de exame',
status: 'Finalizado',
createdAt: '2026-03-20T09:00:00.000Z',
author: 'Dr. Carlos Mendes',
summary: 'Resultado complementar revisado pela equipe assistencial.',
},
] ]
}, },
} }
function readRecords() {
if (typeof window === 'undefined') return INITIAL_RECORDS.map(normalizeRecord)
try {
const raw = window.localStorage.getItem(STORAGE_KEY)
if (!raw) {
const initial = INITIAL_RECORDS.map(normalizeRecord)
writeRecords(initial)
return initial
}
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed)) return INITIAL_RECORDS.map(normalizeRecord)
return parsed.map(normalizeRecord).sort(sortByDateDesc)
} catch {
return INITIAL_RECORDS.map(normalizeRecord)
}
}
function writeRecords(records) {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(records.map(normalizeRecord).sort(sortByDateDesc)))
} catch {
// Local persistence is best-effort while the module is still mock-backed.
}
}
function normalizeRecord(record) {
const dateTime = record.dateTime || parseLegacyDate(record.date) || toLocalInputValue(new Date())
const summary = record.summary || record.orientations || record.diagnosticReasoning || 'Registro de prontuario sem resumo.'
return {
id: String(record.id || `record-${Date.now()}`),
patientId: String(record.patientId || ''),
patient: record.patient || 'Paciente sem nome',
patientDocument: record.patientDocument || record.document || '',
patientEmail: record.patientEmail || record.email || '',
patientPhone: record.patientPhone || record.phone || '',
dateTime,
date: record.date || formatDateTime(dateTime),
createdAt: record.createdAt || toIso(dateTime),
updatedAt: record.updatedAt || record.createdAt || toIso(dateTime),
doctor: record.doctor || record.professional || 'Profissional nao informado',
type: record.type || 'Primeira Consulta',
cid: record.cid || 'CID nao informado',
status: record.status === 'rascunho' ? 'rascunho' : 'completo',
summary,
diagnosticReasoning: record.diagnosticReasoning || record.anamnesis || summary,
diagnosticHypotheses: record.diagnosticHypotheses || record.cid || 'Hipoteses diagnosticas nao informadas.',
definitiveDiagnosis: record.definitiveDiagnosis || record.cid || 'Diagnostico definitivo nao informado.',
prescriptions: record.prescriptions || 'Prescricao nao informada.',
procedures: record.procedures || record.physicalExam || 'Procedimentos nao informados.',
surgeries: record.surgeries || 'Cirurgias nao informadas ou nao se aplica.',
orientations: record.orientations || record.conduct || 'Orientacoes nao informadas.',
labResults: record.labResults || 'Laudos laboratoriais nao informados.',
imageResults: record.imageResults || 'Laudos de imagem nao informados.',
multiprofessionalNotes: record.multiprofessionalNotes || 'Notas multiprofissionais nao informadas.',
signature: record.signature || record.doctor || 'Assinatura nao informada.',
professionalStamp: record.professionalStamp || `Registro assinado por ${record.doctor || 'profissional nao informado'}.`,
}
}
function sortByDateDesc(a, b) {
return new Date(b.dateTime || b.createdAt).getTime() - new Date(a.dateTime || a.createdAt).getTime()
}
function parseLegacyDate(value) {
const match = String(value || '').match(/^(\d{2})\/(\d{2})\/(\d{4})$/)
if (!match) return ''
return `${match[3]}-${match[2]}-${match[1]}T09:00`
}
function toIso(value) {
const parsed = new Date(value)
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString()
}
function toLocalInputValue(date) {
const parsed = date instanceof Date ? date : new Date(date)
const safeDate = Number.isNaN(parsed.getTime()) ? new Date() : parsed
const year = safeDate.getFullYear()
const month = String(safeDate.getMonth() + 1).padStart(2, '0')
const day = String(safeDate.getDate()).padStart(2, '0')
const hours = String(safeDate.getHours()).padStart(2, '0')
const minutes = String(safeDate.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
function formatDateTime(value) {
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) return 'Data nao informada'
return parsed.toLocaleString('pt-BR')
}