Finalizando merge da branch develop com origin/develop
This commit is contained in:
commit
9c7ce7d7d2
576
package-lock.json
generated
Normal file
576
package-lock.json
generated
Normal file
@ -0,0 +1,576 @@
|
||||
{
|
||||
"name": "riseup-squad20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.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==",
|
||||
"license": "MIT",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
|
||||
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.20.2",
|
||||
"@react-aria/interactions": "^3.25.0",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">= 16 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
|
||||
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/interactions": "^3.25.5",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
|
||||
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
|
||||
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.10.8",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/flags": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
|
||||
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
|
||||
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-arithmetic": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz",
|
||||
"integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/globalize": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz",
|
||||
"integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA=="
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar": {
|
||||
"version": "1.19.4",
|
||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.19.4.tgz",
|
||||
"integrity": "sha512-FrvbDx2LF6JAWFD96LU1jjloppC5OgIvMYUYIPzAw5Aq+ArYFPxAjLqXc4DyxfsQDN0TJTMuS/BIbcSB7Pg0YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"clsx": "^1.2.1",
|
||||
"date-arithmetic": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dom-helpers": "^5.2.1",
|
||||
"globalize": "^0.1.1",
|
||||
"invariant": "^2.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.2.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.40",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-overlays": "^5.2.1",
|
||||
"uncontrollable": "^7.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17 || ^18 || ^19",
|
||||
"react-dom": "^16.14.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-overlays": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
|
||||
"integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.8",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@restart/hooks": "^0.4.7",
|
||||
"@types/warning": "^3.0.0",
|
||||
"dom-helpers": "^5.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/uncontrollable": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/react": ">=16.9.11",
|
||||
"invariant": "^2.2.4",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4"
|
||||
}
|
||||
}
|
||||
139
susconecta/app/agendamento/page.tsx
Normal file
139
susconecta/app/agendamento/page.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
|
||||
const AgendaCalendar = dynamic(() => import('@/components/agendamento/AgendaCalendar'), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-6"></div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const AppointmentModal = dynamic(() => import('@/components/agendamento/AppointmentModal'), { ssr: false });
|
||||
const ListaEspera = dynamic(() => import('@/components/agendamento/ListaEspera'), { ssr: false });
|
||||
|
||||
|
||||
const mockAppointments = [
|
||||
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
||||
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
||||
{ id: '3', patient: 'Mariana Lima', time: '2025-09-10T14:00', duration: 60, type: 'exame' as const, status: 'confirmed' as const, professional: '3', notes: '' },
|
||||
];
|
||||
|
||||
const mockWaitingList = [
|
||||
{ id: '1', name: 'Ana Costa', specialty: 'Cardiologia', preferredDate: '2025-09-12', priority: 'high' as const, contact: '(11) 99999-9999' },
|
||||
{ id: '2', name: 'Pedro Alves', specialty: 'Dermatologia', preferredDate: '2025-09-15', priority: 'medium' as const, contact: '(11) 98888-8888' },
|
||||
{ id: '3', name: 'Mariana Lima', specialty: 'Ortopedia', preferredDate: '2025-09-20', priority: 'low' as const, contact: '(11) 97777-7777' },
|
||||
];
|
||||
|
||||
const mockProfessionals = [
|
||||
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
|
||||
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
|
||||
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
|
||||
];
|
||||
|
||||
export default function AgendamentoPage() {
|
||||
const [appointments, setAppointments] = useState(mockAppointments);
|
||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
|
||||
const [activeTab, setActiveTab] = useState<'agenda' | 'espera'>('agenda');
|
||||
|
||||
const handleSaveAppointment = (appointment: any) => {
|
||||
if (appointment.id) {
|
||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||
} else {
|
||||
const newAppointment = {
|
||||
...appointment,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
setAppointments(prev => [...prev, newAppointment]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAppointment = (appointment: any) => {
|
||||
setSelectedAppointment(appointment);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAddAppointment = () => {
|
||||
setSelectedAppointment(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedAppointment(null);
|
||||
};
|
||||
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Agendamento</h1>
|
||||
<p className="text-gray-600 mt-2">Gerencie a agenda da clínica</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('agenda')}
|
||||
className={`px-4 py-2 rounded-md ${
|
||||
activeTab === 'agenda'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Agenda
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('espera')}
|
||||
className={`px-4 py-2 rounded-md ${
|
||||
activeTab === 'espera'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Lista de Espera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'agenda' ? (
|
||||
<AgendaCalendar
|
||||
professionals={mockProfessionals}
|
||||
appointments={appointments}
|
||||
onAddAppointment={handleAddAppointment}
|
||||
onEditAppointment={handleEditAppointment}
|
||||
/>
|
||||
) : (
|
||||
<ListaEspera
|
||||
patients={waitingList}
|
||||
onNotify={handleNotifyPatient}
|
||||
onAddToWaitlist={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AppointmentModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveAppointment}
|
||||
professionals={mockProfessionals}
|
||||
appointment={selectedAppointment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,381 +1,251 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Search, Filter, Plus, MoreHorizontal, Calendar, Gift, Eye, Edit, Trash2, CalendarPlus } from "lucide-react"
|
||||
"use client";
|
||||
|
||||
const patients = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Aaron Avalos Perez",
|
||||
phone: "(75) 99982-6363",
|
||||
city: "Aracaju",
|
||||
state: "Sergipe",
|
||||
lastAppointment: "26/09/2025 14:30",
|
||||
nextAppointment: "19/08/2025 15:00",
|
||||
isVip: false,
|
||||
convenio: "unimed",
|
||||
birthday: "1985-03-15",
|
||||
age: 40,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "ABENANDO OLIVEIRA DE JESUS",
|
||||
phone: "(75) 99986-0093",
|
||||
city: "-",
|
||||
state: "-",
|
||||
lastAppointment: "Ainda não houve atendimento",
|
||||
nextAppointment: "Nenhum atendimento agendado",
|
||||
isVip: false,
|
||||
convenio: "particular",
|
||||
birthday: "1978-12-03",
|
||||
age: 46,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "ABDIAS DANTAS DOS SANTOS",
|
||||
phone: "(75) 99125-7267",
|
||||
city: "São Cristóvão",
|
||||
state: "Sergipe",
|
||||
lastAppointment: "30/12/2024 08:40",
|
||||
nextAppointment: "Nenhum atendimento agendado",
|
||||
isVip: true,
|
||||
convenio: "bradesco",
|
||||
birthday: "1990-12-03",
|
||||
age: 34,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Abdias Matheus Rodrigues Ferreira",
|
||||
phone: "(75) 99983-7711",
|
||||
city: "Pirambu",
|
||||
state: "Sergipe",
|
||||
lastAppointment: "04/09/2024 16:20",
|
||||
nextAppointment: "Nenhum atendimento agendado",
|
||||
isVip: false,
|
||||
convenio: "amil",
|
||||
birthday: "1982-12-03",
|
||||
age: 42,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Abdon Ferreira Guerra",
|
||||
phone: "(75) 99971-0228",
|
||||
city: "-",
|
||||
state: "-",
|
||||
lastAppointment: "08/05/2025 08:00",
|
||||
nextAppointment: "Nenhum atendimento agendado",
|
||||
isVip: false,
|
||||
convenio: "unimed",
|
||||
birthday: "1975-12-03",
|
||||
age: 49,
|
||||
},
|
||||
]
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
|
||||
|
||||
import { Paciente, Endereco, listarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
||||
|
||||
|
||||
function normalizePaciente(p: any): Paciente {
|
||||
const endereco: Endereco = {
|
||||
cep: p.endereco?.cep ?? p.cep ?? "",
|
||||
logradouro: p.endereco?.logradouro ?? p.logradouro ?? "",
|
||||
numero: p.endereco?.numero ?? p.numero ?? "",
|
||||
complemento: p.endereco?.complemento ?? p.complemento ?? "",
|
||||
bairro: p.endereco?.bairro ?? p.bairro ?? "",
|
||||
cidade: p.endereco?.cidade ?? p.cidade ?? "",
|
||||
estado: p.endereco?.estado ?? p.estado ?? "",
|
||||
};
|
||||
|
||||
return {
|
||||
id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""),
|
||||
nome: p.nome ?? "",
|
||||
nome_social: p.nome_social ?? null,
|
||||
cpf: p.cpf ?? "",
|
||||
rg: p.rg ?? null,
|
||||
sexo: p.sexo ?? null,
|
||||
data_nascimento: p.data_nascimento ?? null,
|
||||
telefone: p.telefone ?? "",
|
||||
email: p.email ?? "",
|
||||
endereco,
|
||||
observacoes: p.observacoes ?? null,
|
||||
foto_url: p.foto_url ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export default function PacientesPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [selectedConvenio, setSelectedConvenio] = useState("all") // Updated default value to "all"
|
||||
const [showVipOnly, setShowVipOnly] = useState(false)
|
||||
const [showBirthdays, setShowBirthdays] = useState(false)
|
||||
const [advancedFilters, setAdvancedFilters] = useState({
|
||||
city: "",
|
||||
state: "",
|
||||
minAge: "",
|
||||
maxAge: "",
|
||||
lastAppointmentFrom: "",
|
||||
lastAppointmentTo: "",
|
||||
})
|
||||
const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false)
|
||||
const [patients, setPatients] = useState<Paciente[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const filteredPatients = patients.filter((patient) => {
|
||||
const matchesSearch =
|
||||
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) || patient.phone.includes(searchTerm)
|
||||
const [search, setSearch] = useState("");
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
|
||||
const matchesConvenio = selectedConvenio === "all" || patient.convenio === selectedConvenio
|
||||
const matchesVip = !showVipOnly || patient.isVip
|
||||
async function loadAll() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await listarPacientes({ page: 1, limit: 20 });
|
||||
setPatients((data ?? []).map(normalizePaciente));
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
setPatients([]);
|
||||
setError(e?.message || "Erro ao carregar pacientes.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if patient has birthday this month
|
||||
const currentMonth = new Date().getMonth() + 1
|
||||
const patientBirthMonth = new Date(patient.birthday).getMonth() + 1
|
||||
const matchesBirthday = !showBirthdays || patientBirthMonth === currentMonth
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
}, []);
|
||||
|
||||
// Advanced filters
|
||||
const matchesCity = !advancedFilters.city || patient.city.toLowerCase().includes(advancedFilters.city.toLowerCase())
|
||||
const matchesState =
|
||||
!advancedFilters.state || patient.state.toLowerCase().includes(advancedFilters.state.toLowerCase())
|
||||
const matchesMinAge = !advancedFilters.minAge || patient.age >= Number.parseInt(advancedFilters.minAge)
|
||||
const matchesMaxAge = !advancedFilters.maxAge || patient.age <= Number.parseInt(advancedFilters.maxAge)
|
||||
const filtered = useMemo(() => {
|
||||
if (!search.trim()) return patients;
|
||||
const q = search.toLowerCase();
|
||||
const qDigits = q.replace(/\D/g, "");
|
||||
return patients.filter((p) => {
|
||||
const byName = (p.nome || "").toLowerCase().includes(q);
|
||||
const byCPF = (p.cpf || "").replace(/\D/g, "").includes(qDigits);
|
||||
const byId = String(p.id || "").includes(qDigits);
|
||||
return byName || byCPF || byId;
|
||||
});
|
||||
}, [patients, search]);
|
||||
|
||||
function handleAdd() {
|
||||
setEditingId(null);
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
function handleEdit(id: string) {
|
||||
setEditingId(id);
|
||||
setShowForm(true);
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("Excluir este paciente?")) return;
|
||||
try {
|
||||
await excluirPaciente(id);
|
||||
setPatients((prev) => prev.filter((x) => String(x.id) !== String(id)));
|
||||
} catch (e: any) {
|
||||
alert(e?.message || "Não foi possível excluir.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaved(p: Paciente) {
|
||||
const saved = normalizePaciente(p);
|
||||
setPatients((prev) => {
|
||||
const i = prev.findIndex((x) => String(x.id) === String(saved.id));
|
||||
if (i < 0) return [saved, ...prev];
|
||||
const clone = [...prev];
|
||||
clone[i] = saved;
|
||||
return clone;
|
||||
});
|
||||
setShowForm(false);
|
||||
}
|
||||
|
||||
async function handleBuscarServidor() {
|
||||
const q = search.trim();
|
||||
if (!q) return loadAll();
|
||||
|
||||
|
||||
if (/^\d+$/.test(q)) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const one = await buscarPacientePorId(q);
|
||||
setPatients(one ? [normalizePaciente(one)] : []);
|
||||
setError(one ? null : "Paciente não encontrado.");
|
||||
} catch (e: any) {
|
||||
setPatients([]);
|
||||
setError(e?.message || "Paciente não encontrado.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await loadAll();
|
||||
setTimeout(() => setSearch(q), 0);
|
||||
}
|
||||
|
||||
if (loading) return <p>Carregando pacientes...</p>;
|
||||
if (error) return <p className="text-red-500">{error}</p>;
|
||||
|
||||
if (showForm) {
|
||||
return (
|
||||
matchesSearch &&
|
||||
matchesConvenio &&
|
||||
matchesVip &&
|
||||
matchesBirthday &&
|
||||
matchesCity &&
|
||||
matchesState &&
|
||||
matchesMinAge &&
|
||||
matchesMaxAge
|
||||
)
|
||||
})
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" onClick={() => setShowForm(false)}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">{editingId ? "Editar paciente" : "Novo paciente"}</h1>
|
||||
</div>
|
||||
|
||||
const clearAdvancedFilters = () => {
|
||||
setAdvancedFilters({
|
||||
city: "",
|
||||
state: "",
|
||||
minAge: "",
|
||||
maxAge: "",
|
||||
lastAppointmentFrom: "",
|
||||
lastAppointmentTo: "",
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewDetails = (patientId: number) => {
|
||||
console.log("[v0] Ver detalhes do paciente:", patientId)
|
||||
// TODO: Navigate to patient details page
|
||||
}
|
||||
|
||||
const handleEditPatient = (patientId: number) => {
|
||||
console.log("[v0] Editar paciente:", patientId)
|
||||
// TODO: Navigate to edit patient form
|
||||
}
|
||||
|
||||
const handleScheduleAppointment = (patientId: number) => {
|
||||
console.log("[v0] Marcar consulta para paciente:", patientId)
|
||||
// TODO: Open appointment scheduling modal
|
||||
}
|
||||
|
||||
const handleDeletePatient = (patientId: number) => {
|
||||
console.log("[v0] Excluir paciente:", patientId)
|
||||
// TODO: Show confirmation dialog and delete patient
|
||||
<PatientRegistrationForm
|
||||
inline
|
||||
mode={editingId ? "edit" : "create"}
|
||||
patientId={editingId ? Number(editingId) : null}
|
||||
onSaved={handleSaved}
|
||||
onClose={() => setShowForm(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
{}
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Pacientes</h1>
|
||||
<p className="text-muted-foreground">Gerencie as informações de seus pacientes</p>
|
||||
</div>
|
||||
<Button className="bg-primary hover:bg-primary/90">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Adicionar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div className="relative flex-1 min-w-64">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar paciente"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
<h1 className="text-2xl font-bold">Pacientes</h1>
|
||||
<p className="text-muted-foreground">Gerencie os pacientes</p>
|
||||
</div>
|
||||
|
||||
<Select value={selectedConvenio} onValueChange={setSelectedConvenio}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Selecione o Convênio" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos os Convênios</SelectItem>
|
||||
<SelectItem value="unimed">Unimed</SelectItem>
|
||||
<SelectItem value="bradesco">Bradesco Saúde</SelectItem>
|
||||
<SelectItem value="amil">Amil</SelectItem>
|
||||
<SelectItem value="particular">Particular</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant={showVipOnly ? "default" : "outline"}
|
||||
onClick={() => setShowVipOnly(!showVipOnly)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Gift className="h-4 w-4" />
|
||||
VIP
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={showBirthdays ? "default" : "outline"}
|
||||
onClick={() => setShowBirthdays(!showBirthdays)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Calendar className="h-4 w-4" />
|
||||
Aniversariantes
|
||||
</Button>
|
||||
|
||||
<Dialog open={isAdvancedFilterOpen} onOpenChange={setIsAdvancedFilterOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2 bg-transparent">
|
||||
<Filter className="h-4 w-4" />
|
||||
Filtro avançado
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Filtros Avançados</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use os filtros abaixo para refinar sua busca por pacientes específicos.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Cidade</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={advancedFilters.city}
|
||||
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, city: e.target.value }))}
|
||||
placeholder="Digite a cidade"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="state">Estado</Label>
|
||||
<Input
|
||||
id="state"
|
||||
value={advancedFilters.state}
|
||||
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, state: e.target.value }))}
|
||||
placeholder="Digite o estado"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="minAge">Idade mínima</Label>
|
||||
<Input
|
||||
id="minAge"
|
||||
type="number"
|
||||
value={advancedFilters.minAge}
|
||||
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, minAge: e.target.value }))}
|
||||
placeholder="Ex: 18"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxAge">Idade máxima</Label>
|
||||
<Input
|
||||
id="maxAge"
|
||||
type="number"
|
||||
value={advancedFilters.maxAge}
|
||||
onChange={(e) => setAdvancedFilters((prev) => ({ ...prev, maxAge: e.target.value }))}
|
||||
placeholder="Ex: 65"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button onClick={clearAdvancedFilters} variant="outline" className="flex-1 bg-transparent">
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
<Button onClick={() => setIsAdvancedFilterOpen(false)} className="flex-1">
|
||||
Aplicar Filtros
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-8 w-80"
|
||||
placeholder="Buscar por nome, CPF ou ID…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={handleBuscarServidor}>Buscar</Button>
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Novo paciente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nome</TableHead>
|
||||
<TableHead>CPF</TableHead>
|
||||
<TableHead>Telefone</TableHead>
|
||||
<TableHead>Cidade</TableHead>
|
||||
<TableHead>Estado</TableHead>
|
||||
<TableHead>Último atendimento</TableHead>
|
||||
<TableHead>Próximo atendimento</TableHead>
|
||||
<TableHead className="w-[100px]">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPatients.map((patient) => (
|
||||
<TableRow key={patient.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-muted rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-medium">{patient.name.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
<button onClick={() => handleViewDetails(patient.id)} className="hover:text-primary cursor-pointer">
|
||||
{patient.name}
|
||||
</button>
|
||||
{patient.isVip && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
VIP
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{patient.phone}</TableCell>
|
||||
<TableCell>{patient.city}</TableCell>
|
||||
<TableCell>{patient.state}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={patient.lastAppointment === "Ainda não houve atendimento" ? "text-muted-foreground" : ""}
|
||||
>
|
||||
{patient.lastAppointment}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={patient.nextAppointment === "Nenhum atendimento agendado" ? "text-muted-foreground" : ""}
|
||||
>
|
||||
{patient.nextAppointment}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Abrir menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleViewDetails(patient.id)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Ver detalhes
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEditPatient(patient.id)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleScheduleAppointment(patient.id)}>
|
||||
<CalendarPlus className="mr-2 h-4 w-4" />
|
||||
Marcar consulta
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDeletePatient(patient.id)} className="text-destructive">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{filtered.length > 0 ? (
|
||||
filtered.map((p) => (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="font-medium">{p.nome || "(sem nome)"}</TableCell>
|
||||
<TableCell>{p.cpf || "-"}</TableCell>
|
||||
<TableCell>{p.telefone || "-"}</TableCell>
|
||||
<TableCell>{p.endereco?.cidade || "-"}</TableCell>
|
||||
<TableCell>{p.endereco?.estado || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-accent">
|
||||
<span className="sr-only">Abrir menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => alert(JSON.stringify(p, null, 2))}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Ver
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(String(p.id))}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||
Nenhum paciente encontrado
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Mostrando {filteredPatients.length} de {patients.length} pacientes
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,26 +1,13 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
import "./globals.css"
|
||||
|
||||
const geistSans = Geist({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-geist-sans",
|
||||
})
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-geist-mono",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SUSConecta - Conectando Pacientes e Profissionais de Saúde",
|
||||
description:
|
||||
"Plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada. Experimente o futuro dos agendamentos médicos.",
|
||||
keywords: "saúde, médicos, pacientes, agendamento, telemedicina, SUS",
|
||||
generator: 'v0.app'
|
||||
generator: 'v0.app'
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
@ -29,8 +16,8 @@ export default function RootLayout({
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="pt-BR" className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<body>{children}</body>
|
||||
<html lang="pt-BR" className="antialiased">
|
||||
<body style={{ fontFamily: "var(--font-geist-sans)" }}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
858
susconecta/app/profissional/page.tsx
Normal file
858
susconecta/app/profissional/page.tsx
Normal file
@ -0,0 +1,858 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock } from "lucide-react"
|
||||
import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
||||
|
||||
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const pacientes = [
|
||||
{ nome: "Ana Souza", cpf: "123.456.789-00", idade: 42, statusLaudo: "Finalizado" },
|
||||
{ nome: "Bruno Lima", cpf: "987.654.321-00", idade: 33, statusLaudo: "Pendente" },
|
||||
{ nome: "Carla Menezes", cpf: "111.222.333-44", idade: 67, statusLaudo: "Rascunho" },
|
||||
];
|
||||
|
||||
const medico = {
|
||||
nome: "Dr. Carlos Andrade",
|
||||
identificacao: "CRM 000000 • Cardiologia",
|
||||
fotoUrl: "",
|
||||
}
|
||||
|
||||
|
||||
const colorsByType = {
|
||||
Rotina: "#4dabf7",
|
||||
Cardiologia: "#f76c6c",
|
||||
Otorrino: "#f7b84d",
|
||||
Pediatria: "#6cf78b",
|
||||
Dermatologia: "#9b59b6",
|
||||
Oftalmologia: "#2ecc71"
|
||||
};
|
||||
|
||||
const ProfissionalPage = () => {
|
||||
const [activeSection, setActiveSection] = useState('calendario');
|
||||
const [pacienteSelecionado, setPacienteSelecionado] = useState<any>(null);
|
||||
const [events, setEvents] = useState<any[]>([
|
||||
|
||||
{
|
||||
id: 1,
|
||||
title: "Ana Souza",
|
||||
type: "Cardiologia",
|
||||
time: "09:00",
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
pacienteId: "123.456.789-00",
|
||||
color: colorsByType.Cardiologia
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Bruno Lima",
|
||||
type: "Rotina",
|
||||
time: "10:30",
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
pacienteId: "987.654.321-00",
|
||||
color: colorsByType.Rotina
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Carla Menezes",
|
||||
type: "Dermatologia",
|
||||
time: "14:00",
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
pacienteId: "111.222.333-44",
|
||||
color: colorsByType.Dermatologia
|
||||
}
|
||||
]);
|
||||
const [editingEvent, setEditingEvent] = useState<any>(null);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [showActionModal, setShowActionModal] = useState(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const [newEvent, setNewEvent] = useState({
|
||||
title: "",
|
||||
type: "",
|
||||
time: "",
|
||||
pacienteId: ""
|
||||
});
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||
const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date());
|
||||
|
||||
const handleSave = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
console.log("Laudo salvo!");
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const handleAbrirProntuario = (paciente: any) => {
|
||||
setPacienteSelecionado(paciente);
|
||||
|
||||
const pacienteLaudo = document.getElementById('pacienteLaudo') as HTMLInputElement;
|
||||
if (pacienteLaudo) pacienteLaudo.value = paciente.nome;
|
||||
|
||||
const destinatario = document.getElementById('destinatario') as HTMLInputElement;
|
||||
if (destinatario) destinatario.value = `${paciente.nome} - ${paciente.cpf}`;
|
||||
|
||||
const prontuarioSection = document.getElementById('prontuario-paciente');
|
||||
if (prontuarioSection) {
|
||||
prontuarioSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFecharProntuario = () => {
|
||||
setPacienteSelecionado(null);
|
||||
};
|
||||
|
||||
|
||||
const navigateDate = (direction: 'prev' | 'next') => {
|
||||
const newDate = new Date(currentCalendarDate);
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
|
||||
setCurrentCalendarDate(newDate);
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
setCurrentCalendarDate(new Date());
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('pt-BR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Filtrar eventos do dia atual
|
||||
const getTodayEvents = () => {
|
||||
const today = currentCalendarDate.toISOString().split('T')[0];
|
||||
return events
|
||||
.filter(event => event.date === today)
|
||||
.sort((a, b) => a.time.localeCompare(b.time));
|
||||
};
|
||||
|
||||
const getStatusColor = (type: string) => {
|
||||
return colorsByType[type as keyof typeof colorsByType] || "#4dabf7";
|
||||
};
|
||||
|
||||
|
||||
const handleDateClick = (arg: any) => {
|
||||
setSelectedDate(arg.dateStr);
|
||||
setNewEvent({ title: "", type: "", time: "", pacienteId: "" });
|
||||
setStep(1);
|
||||
setEditingEvent(null);
|
||||
setShowPopup(true);
|
||||
};
|
||||
|
||||
|
||||
const handleAddEvent = () => {
|
||||
const paciente = pacientes.find(p => p.nome === newEvent.title);
|
||||
const eventToAdd = {
|
||||
id: Date.now(),
|
||||
title: newEvent.title,
|
||||
type: newEvent.type,
|
||||
time: newEvent.time,
|
||||
date: selectedDate || currentCalendarDate.toISOString().split('T')[0],
|
||||
pacienteId: paciente ? paciente.cpf : "",
|
||||
color: colorsByType[newEvent.type as keyof typeof colorsByType] || "#4dabf7"
|
||||
};
|
||||
setEvents((prev) => [...prev, eventToAdd]);
|
||||
setShowPopup(false);
|
||||
};
|
||||
|
||||
|
||||
const handleEditEvent = () => {
|
||||
setEvents((prevEvents) =>
|
||||
prevEvents.map((ev) =>
|
||||
ev.id.toString() === editingEvent.id.toString()
|
||||
? {
|
||||
...ev,
|
||||
title: newEvent.title,
|
||||
type: newEvent.type,
|
||||
time: newEvent.time,
|
||||
color: colorsByType[newEvent.type as keyof typeof colorsByType] || "#4dabf7"
|
||||
}
|
||||
: ev
|
||||
)
|
||||
);
|
||||
setEditingEvent(null);
|
||||
setShowPopup(false);
|
||||
setShowActionModal(false);
|
||||
};
|
||||
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (step < 3) setStep(step + 1);
|
||||
else editingEvent ? handleEditEvent() : handleAddEvent();
|
||||
};
|
||||
|
||||
|
||||
const handleEventClick = (clickInfo: any) => {
|
||||
setSelectedEvent(clickInfo.event);
|
||||
setShowActionModal(true);
|
||||
};
|
||||
|
||||
|
||||
const handleDeleteEvent = () => {
|
||||
if (!selectedEvent) return;
|
||||
setEvents((prevEvents) =>
|
||||
prevEvents.filter((ev: any) => ev.id.toString() !== selectedEvent.id.toString())
|
||||
);
|
||||
setShowActionModal(false);
|
||||
};
|
||||
|
||||
|
||||
const handleStartEdit = () => {
|
||||
if (!selectedEvent) return;
|
||||
setEditingEvent(selectedEvent);
|
||||
setNewEvent({
|
||||
title: selectedEvent.title,
|
||||
type: selectedEvent.extendedProps.type,
|
||||
time: selectedEvent.extendedProps.time,
|
||||
pacienteId: selectedEvent.extendedProps.pacienteId || ""
|
||||
});
|
||||
setStep(1);
|
||||
setShowActionModal(false);
|
||||
setShowPopup(true);
|
||||
};
|
||||
|
||||
|
||||
const renderEventContent = (eventInfo: any) => {
|
||||
const bg = eventInfo.event.backgroundColor || eventInfo.event.extendedProps?.color || "#4dabf7";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 text-xs p-1 rounded cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: bg,
|
||||
color: "#fff",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
title={`${eventInfo.event.title} • ${eventInfo.event.extendedProps.type} • ${eventInfo.event.extendedProps.time}`}
|
||||
>
|
||||
<span className="truncate">{eventInfo.event.title}</span>
|
||||
<span>•</span>
|
||||
<span className="truncate">{eventInfo.event.extendedProps.type}</span>
|
||||
<span>•</span>
|
||||
<span>{eventInfo.event.extendedProps.time}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const renderCalendarioSection = () => {
|
||||
const todayEvents = getTodayEvents();
|
||||
|
||||
return (
|
||||
<section className="bg-white shadow-md rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold">Agenda do Dia</h2>
|
||||
</div>
|
||||
|
||||
{/* Navegação de Data */}
|
||||
<div className="flex items-center justify-between mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDate('prev')}
|
||||
className="p-2 hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{formatDate(currentCalendarDate)}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-2 hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={goToToday}
|
||||
className="ml-4 px-3 py-1 text-sm hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
>
|
||||
Hoje
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Pacientes do Dia */}
|
||||
<div className="space-y-4">
|
||||
{todayEvents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<CalendarIcon className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-lg mb-2">Nenhuma consulta agendada para este dia</p>
|
||||
<p className="text-sm">Agenda livre para este dia</p>
|
||||
</div>
|
||||
) : (
|
||||
todayEvents.map((appointment) => {
|
||||
const paciente = pacientes.find(p => p.nome === appointment.title);
|
||||
return (
|
||||
<div
|
||||
key={appointment.id}
|
||||
className="border-l-4 p-4 rounded-lg shadow-sm bg-white border-gray-200"
|
||||
style={{ borderLeftColor: getStatusColor(appointment.type) }}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mr-3"
|
||||
style={{ backgroundColor: getStatusColor(appointment.type) }}
|
||||
></div>
|
||||
<div>
|
||||
<div className="font-medium flex items-center">
|
||||
<User className="h-4 w-4 mr-2 text-gray-500" />
|
||||
{appointment.title}
|
||||
</div>
|
||||
{paciente && (
|
||||
<div className="text-sm text-gray-500">
|
||||
CPF: {paciente.cpf} • {paciente.idade} anos
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-2 text-gray-500" />
|
||||
<span className="font-medium">{appointment.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="px-3 py-1 rounded-full text-sm font-medium text-white"
|
||||
style={{ backgroundColor: getStatusColor(appointment.type) }}
|
||||
>
|
||||
{appointment.type}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-primary text-primary hover:bg-primary hover:text-white cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (paciente) {
|
||||
handleAbrirProntuario(paciente);
|
||||
setActiveSection('prontuario');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-900 text-white text-xs rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
|
||||
Ver informações do paciente
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const renderPacientesSection = () => (
|
||||
<div className="bg-white shadow-md rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Gerenciamento de Pacientes</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Paciente</TableHead>
|
||||
<TableHead>CPF</TableHead>
|
||||
<TableHead>Idade</TableHead>
|
||||
<TableHead>Status do laudo</TableHead>
|
||||
<TableHead>Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pacientes.map((paciente) => (
|
||||
<TableRow key={paciente.cpf}>
|
||||
<TableCell className="font-medium">{paciente.nome}</TableCell>
|
||||
<TableCell>{paciente.cpf}</TableCell>
|
||||
<TableCell>{paciente.idade}</TableCell>
|
||||
<TableCell>{paciente.statusLaudo}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-primary text-primary hover:bg-primary hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
handleAbrirProntuario(paciente);
|
||||
setActiveSection('prontuario');
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1 bg-gray-900 text-white text-xs rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
|
||||
Ver informações do paciente
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const renderProntuarioSection = () => (
|
||||
<div className="bg-white shadow-md rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Prontuário do Paciente</h2>
|
||||
{pacienteSelecionado && (
|
||||
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4 mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="font-semibold text-primary">Dados do Paciente</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleFecharProntuario}
|
||||
className="text-primary hover:text-primary hover:bg-primary/10 h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-primary">Nome:</span>
|
||||
<p className="text-primary/80">{pacienteSelecionado.nome}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-primary">CPF:</span>
|
||||
<p className="text-primary/80">{pacienteSelecionado.cpf}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-primary">Idade:</span>
|
||||
<p className="text-primary/80">{pacienteSelecionado.idade} anos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="dataConsulta">Data da consulta</Label>
|
||||
<p id="dataConsulta" className="text-sm text-muted-foreground">
|
||||
03/09/2025
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cid10">CID-10</Label>
|
||||
<Input id="cid10" placeholder="Insira o código CID-10" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retornoAgendado">Retorno Agendado</Label>
|
||||
<Input id="retornoAgendado" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="anamnese">Anamnese</Label>
|
||||
<Textarea id="anamnese" placeholder="Descreva a anamnese do paciente" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="exameFisico">Exame Físico</Label>
|
||||
<Textarea id="exameFisico" placeholder="Descreva o exame físico" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hipoteses">Hipóteses Diagnósticas</Label>
|
||||
<Textarea id="hipoteses" placeholder="Liste as hipóteses diagnósticas" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="conduta">Conduta Médica</Label>
|
||||
<Textarea id="conduta" placeholder="Descreva a conduta médica" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="prescricoes">Prescrições</Label>
|
||||
<Textarea id="prescricoes" placeholder="Insira as prescrições" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave}>Salvar Informações</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const renderLaudosSection = () => (
|
||||
<section>
|
||||
<h2>Página em construção</h2>
|
||||
</section>
|
||||
);
|
||||
|
||||
|
||||
const renderComunicacaoSection = () => (
|
||||
<div className="bg-white shadow-md rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Comunicação com o Paciente</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="destinatario">Destinatário</Label>
|
||||
<Input
|
||||
id="destinatario"
|
||||
placeholder="Nome do Paciente ou CPF"
|
||||
disabled
|
||||
className="bg-muted cursor-not-allowed text-gray-700 disabled:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tipoMensagem">Tipo de mensagem</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="tipoMensagem" className="hover:border-primary focus:border-primary cursor-pointer">
|
||||
<SelectValue placeholder="Selecione o tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-white border border-gray-200">
|
||||
<SelectItem value="lembrete" className="hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground cursor-pointer">Lembrete de Consulta</SelectItem>
|
||||
<SelectItem value="resultado" className="hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground cursor-pointer">Resultado de Exame</SelectItem>
|
||||
<SelectItem value="instrucao" className="hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground cursor-pointer">Instruções Pós-Consulta</SelectItem>
|
||||
<SelectItem value="outro" className="hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground cursor-pointer">Outro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="dataEnvio">Data de envio</Label>
|
||||
<p id="dataEnvio" className="text-sm text-muted-foreground">03/09/2025</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="statusEntrega">Status da entrega</Label>
|
||||
<p id="statusEntrega" className="text-sm text-muted-foreground">Pendente</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Resposta do paciente</Label>
|
||||
<div className="border rounded-md p-3 bg-muted/40 space-y-2">
|
||||
<p className="text-sm">"Ok, obrigado pelo lembrete!"</p>
|
||||
<p className="text-xs text-muted-foreground">03/09/2025 14:30</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<Button onClick={handleSave}>Registrar Comunicação</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const renderPerfilSection = () => (
|
||||
<section>
|
||||
<h2>Página em construção</h2>
|
||||
</section>
|
||||
);
|
||||
|
||||
|
||||
const renderActiveSection = () => {
|
||||
switch (activeSection) {
|
||||
case 'calendario':
|
||||
return renderCalendarioSection();
|
||||
case 'pacientes':
|
||||
return renderPacientesSection();
|
||||
case 'prontuario':
|
||||
return renderProntuarioSection();
|
||||
case 'laudos':
|
||||
return renderLaudosSection();
|
||||
case 'comunicacao':
|
||||
return renderComunicacaoSection();
|
||||
case 'perfil':
|
||||
return renderPerfilSection();
|
||||
default:
|
||||
return renderCalendarioSection();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<header className="bg-white shadow-md rounded-lg p-4 mb-6 flex items-center gap-4">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={medico.fotoUrl} alt={medico.nome} />
|
||||
<AvatarFallback className="bg-muted">
|
||||
<User className="h-5 w-5" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground truncate">Conta do profissional</p>
|
||||
<h2 className="text-lg font-semibold leading-none truncate">{medico.nome}</h2>
|
||||
<p className="text-sm text-muted-foreground truncate">{medico.identificacao}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">
|
||||
{}
|
||||
<aside className="md:sticky md:top-8 h-fit">
|
||||
<nav className="bg-white shadow-md rounded-lg p-3 space-y-1">
|
||||
<Button
|
||||
variant={activeSection === 'calendario' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('calendario')}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
Calendário
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'pacientes' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('pacientes')}
|
||||
>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Pacientes
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'prontuario' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('prontuario')}
|
||||
>
|
||||
<ClipboardList className="mr-2 h-4 w-4" />
|
||||
Prontuário
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'laudos' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('laudos')}
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Laudos
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'comunicacao' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('comunicacao')}
|
||||
>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Comunicação
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'perfil' ? 'default' : 'ghost'}
|
||||
className="w-full justify-start cursor-pointer hover:bg-primary hover:text-primary-foreground cursor-pointer"
|
||||
onClick={() => setActiveSection('perfil')}
|
||||
>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Meu Perfil
|
||||
</Button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-3xl font-bold">Área do Profissional de Saúde</h1>
|
||||
<Button asChild>
|
||||
<Link href="/">Início</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mb-8">Bem-vindo à sua área exclusiva.</p>
|
||||
|
||||
{renderActiveSection()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{showPopup && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50">
|
||||
|
||||
<div className="bg-white p-6 rounded-lg w-96 border border-black">
|
||||
|
||||
{step === 1 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold mb-2">Selecionar Paciente</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Data: {selectedDate ? new Date(selectedDate + 'T00:00:00').toLocaleDateString('pt-BR') : 'Não selecionada'}
|
||||
</p>
|
||||
<Select
|
||||
value={newEvent.title}
|
||||
onValueChange={(value) => setNewEvent({ ...newEvent, title: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o paciente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pacientes.map((paciente) => (
|
||||
<SelectItem key={paciente.cpf} value={paciente.nome}>
|
||||
{paciente.nome} - {paciente.cpf}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
onClick={() => setShowPopup(false)}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNextStep}
|
||||
disabled={!newEvent.title}
|
||||
className="flex-1"
|
||||
>
|
||||
Próximo
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold mb-4">Tipo da Consulta</h3>
|
||||
<Select
|
||||
value={newEvent.type}
|
||||
onValueChange={(value) => setNewEvent({ ...newEvent, type: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(colorsByType).map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
onClick={() => setStep(1)}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
Voltar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNextStep}
|
||||
disabled={!newEvent.type}
|
||||
className="flex-1"
|
||||
>
|
||||
Próximo
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold mb-4">Horário da Consulta</h3>
|
||||
<Input
|
||||
type="time"
|
||||
value={newEvent.time}
|
||||
onChange={(e) => setNewEvent({ ...newEvent, time: e.target.value })}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => setStep(2)}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
Voltar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNextStep}
|
||||
disabled={!newEvent.time}
|
||||
className="flex-1"
|
||||
>
|
||||
{editingEvent ? "Salvar" : "Agendar"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{showActionModal && selectedEvent && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||
<div className="bg-white p-6 rounded-lg w-96">
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Consulta de {selectedEvent.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{selectedEvent.extendedProps.type} às {selectedEvent.extendedProps.time}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleStartEdit}
|
||||
className="flex-1 flex items-center gap-2"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteEvent}
|
||||
variant="destructive"
|
||||
className="flex-1 flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Excluir
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowActionModal(false)}
|
||||
variant="outline"
|
||||
className="w-full mt-2"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfissionalPage;
|
||||
@ -8,9 +8,9 @@ export function AboutSection() {
|
||||
<section className="py-16 lg:py-24 bg-muted/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Left Content */}
|
||||
{}
|
||||
<div className="space-y-8">
|
||||
{/* Professional Image */}
|
||||
{}
|
||||
<div className="relative">
|
||||
<img
|
||||
src="/Screenshot 2025-09-11 121911.png"
|
||||
@ -19,7 +19,7 @@ export function AboutSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Objective Card */}
|
||||
{}
|
||||
<Card className="bg-primary text-primary-foreground p-8 rounded-2xl">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-primary-foreground/20 rounded-full flex items-center justify-center">
|
||||
@ -36,7 +36,7 @@ export function AboutSection() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Content */}
|
||||
{}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="inline-block px-4 py-2 bg-primary/10 text-primary rounded-full text-sm font-medium uppercase tracking-wide">
|
||||
|
||||
119
susconecta/components/agenda/page.tsx
Normal file
119
susconecta/components/agenda/page.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AgendaCalendar, AppointmentModal, ListaEspera } from '@/components/agendamento';
|
||||
|
||||
|
||||
const mockAppointments = [
|
||||
{ id: '1', patient: 'Ana Costa', time: '2025-09-10T09:00', duration: 30, type: 'consulta' as const, status: 'confirmed' as const, professional: '1', notes: '' },
|
||||
{ id: '2', patient: 'Pedro Alves', time: '2025-09-10T10:30', duration: 45, type: 'retorno' as const, status: 'pending' as const, professional: '2', notes: '' },
|
||||
{ id: '3', patient: 'Mariana Lima', time: '2025-09-10T14:00', duration: 60, type: 'exame' as const, status: 'confirmed' as const, professional: '3', notes: '' },
|
||||
];
|
||||
|
||||
const mockWaitingList = [
|
||||
{ id: '1', name: 'Ana Costa', specialty: 'Cardiologia', preferredDate: '2025-09-12', priority: 'high' as const, contact: '(11) 99999-9999' },
|
||||
{ id: '2', name: 'Pedro Alves', specialty: 'Dermatologia', preferredDate: '2025-09-15', priority: 'medium' as const, contact: '(11) 98888-8888' },
|
||||
{ id: '3', name: 'Mariana Lima', specialty: 'Ortopedia', preferredDate: '2025-09-20', priority: 'low' as const, contact: '(11) 97777-7777' },
|
||||
];
|
||||
|
||||
const mockProfessionals = [
|
||||
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
|
||||
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
|
||||
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
|
||||
];
|
||||
|
||||
export default function AgendaPage() {
|
||||
const [appointments, setAppointments] = useState(mockAppointments);
|
||||
const [waitingList, setWaitingList] = useState(mockWaitingList);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
|
||||
const [activeTab, setActiveTab] = useState<'agenda' | 'espera'>('agenda');
|
||||
|
||||
const handleSaveAppointment = (appointment: any) => {
|
||||
if (appointment.id) {
|
||||
|
||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||
} else {
|
||||
|
||||
const newAppointment = {
|
||||
...appointment,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
setAppointments(prev => [...prev, newAppointment]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAppointment = (appointment: any) => {
|
||||
setSelectedAppointment(appointment);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAddAppointment = () => {
|
||||
setSelectedAppointment(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedAppointment(null);
|
||||
};
|
||||
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Agendamento</h1>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('agenda')}
|
||||
className={`px-4 py-2 rounded-md ${
|
||||
activeTab === 'agenda'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Agenda
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('espera')}
|
||||
className={`px-4 py-2 rounded-md ${
|
||||
activeTab === 'espera'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Lista de Espera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'agenda' ? (
|
||||
<AgendaCalendar
|
||||
professionals={mockProfessionals}
|
||||
appointments={appointments}
|
||||
onAddAppointment={handleAddAppointment}
|
||||
onEditAppointment={handleEditAppointment}
|
||||
/>
|
||||
) : (
|
||||
<ListaEspera
|
||||
patients={waitingList}
|
||||
onNotify={handleNotifyPatient}
|
||||
onAddToWaitlist={() => {/* implementar */}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AppointmentModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveAppointment}
|
||||
professionals={mockProfessionals}
|
||||
appointment={selectedAppointment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
303
susconecta/components/agendamento/AgendaCalendar.tsx
Normal file
303
susconecta/components/agendamento/AgendaCalendar.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Plus, Clock, User, Calendar as CalendarIcon } from 'lucide-react';
|
||||
|
||||
interface Appointment {
|
||||
id: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
professional: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface Professional {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
interface AgendaCalendarProps {
|
||||
professionals: Professional[];
|
||||
appointments: Appointment[];
|
||||
onAddAppointment: () => void;
|
||||
onEditAppointment: (appointment: Appointment) => void;
|
||||
}
|
||||
|
||||
export default function AgendaCalendar({
|
||||
professionals,
|
||||
appointments,
|
||||
onAddAppointment,
|
||||
onEditAppointment
|
||||
}: AgendaCalendarProps) {
|
||||
const [view, setView] = useState<'day' | 'week' | 'month'>('week');
|
||||
const [selectedProfessional, setSelectedProfessional] = useState('all');
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
const timeSlots = Array.from({ length: 11 }, (_, i) => {
|
||||
const hour = i + 8; // Das 8h às 18h
|
||||
return [`${hour.toString().padStart(2, '0')}:00`, `${hour.toString().padStart(2, '0')}:30`];
|
||||
}).flat();
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'confirmed': return 'bg-green-100 border-green-500 text-green-800';
|
||||
case 'pending': return 'bg-yellow-100 border-yellow-500 text-yellow-800';
|
||||
case 'absent': return 'bg-red-100 border-red-500 text-red-800';
|
||||
default: return 'bg-gray-100 border-gray-500 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'consulta': return '🩺';
|
||||
case 'exame': return '📋';
|
||||
case 'retorno': return '↩️';
|
||||
default: return '📅';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('pt-BR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const navigateDate = (direction: 'prev' | 'next') => {
|
||||
const newDate = new Date(currentDate);
|
||||
if (view === 'day') {
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
|
||||
} else if (view === 'week') {
|
||||
newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));
|
||||
} else {
|
||||
newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
|
||||
}
|
||||
setCurrentDate(newDate);
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
setCurrentDate(new Date());
|
||||
};
|
||||
|
||||
|
||||
const filteredAppointments = selectedProfessional === 'all'
|
||||
? appointments
|
||||
: appointments.filter(app => app.professional === selectedProfessional);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Agenda</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<select
|
||||
value={selectedProfessional}
|
||||
onChange={(e) => setSelectedProfessional(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="all">Todos os profissionais</option>
|
||||
{professionals.map(prof => (
|
||||
<option key={prof.id} value={prof.id}>{prof.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="inline-flex rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('day')}
|
||||
className={`px-3 py-2 text-sm font-medium rounded-l-md ${
|
||||
view === 'day'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Dia
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('week')}
|
||||
className={`px-3 py-2 text-sm font-medium -ml-px ${
|
||||
view === 'week'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Semana
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView('month')}
|
||||
className={`px-3 py-2 text-sm font-medium -ml-px rounded-r-md ${
|
||||
view === 'month'
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-white text-gray-700 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Mês
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onAddAppointment}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Novo Agendamento
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => navigateDate('prev')}
|
||||
className="p-1 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{formatDate(currentDate)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => navigateDate('next')}
|
||||
className="p-1 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={goToToday}
|
||||
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
|
||||
>
|
||||
Hoje
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Atalhos: 'C' para calendário, 'F' para fila de espera
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{view !== 'month' && (
|
||||
<div className="overflow-auto">
|
||||
<div className="min-w-full">
|
||||
<div className="flex">
|
||||
<div className="w-20 flex-shrink-0 border-r border-gray-200">
|
||||
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
||||
Hora
|
||||
</div>
|
||||
{timeSlots.map(time => (
|
||||
<div key={time} className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500">
|
||||
{time}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
||||
{currentDate.toLocaleDateString('pt-BR', { weekday: 'long' })}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{timeSlots.map(time => (
|
||||
<div key={time} className="h-16 border-b border-gray-200"></div>
|
||||
))}
|
||||
|
||||
{filteredAppointments.map(app => {
|
||||
const [date, timeStr] = app.time.split('T');
|
||||
const [hours, minutes] = timeStr.split(':');
|
||||
const hour = parseInt(hours);
|
||||
const minute = parseInt(minutes);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={app.id}
|
||||
className={`absolute left-1 right-1 border-l-4 rounded p-2 shadow-sm cursor-pointer ${getStatusColor(app.status)}`}
|
||||
style={{
|
||||
top: `${((hour - 8) * 64 + (minute / 60) * 64) + 48}px`,
|
||||
height: `${(app.duration / 60) * 64}px`,
|
||||
}}
|
||||
onClick={() => onEditAppointment(app)}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="font-medium flex items-center">
|
||||
<User className="h-3 w-3 mr-1" />
|
||||
{app.patient}
|
||||
</div>
|
||||
<div className="text-xs flex items-center mt-1">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
|
||||
</div>
|
||||
<div className="text-xs mt-1">
|
||||
{professionals.find(p => p.id === app.professional)?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs capitalize">
|
||||
{app.status === 'confirmed' ? 'confirmado' : app.status === 'pending' ? 'pendente' : 'ausente'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{view === 'month' && (
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
{filteredAppointments.map(app => {
|
||||
const [date, timeStr] = app.time.split('T');
|
||||
const [hours, minutes] = timeStr.split(':');
|
||||
|
||||
return (
|
||||
<div key={app.id} className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
|
||||
<div className="flex items-center">
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
<span className="font-medium">{app.patient}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
<span>{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm">{professionals.find(p => p.id === app.professional)?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{app.notes && (
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
{app.notes}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
onClick={() => onEditAppointment(app)}
|
||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
227
susconecta/components/agendamento/AppointmentModal.tsx
Normal file
227
susconecta/components/agendamento/AppointmentModal.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface Appointment {
|
||||
id?: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
professional: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface Professional {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
interface AppointmentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (appointment: Appointment) => void;
|
||||
professionals: Professional[];
|
||||
appointment?: Appointment | null;
|
||||
}
|
||||
|
||||
export default function AppointmentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
professionals,
|
||||
appointment
|
||||
}: AppointmentModalProps) {
|
||||
const [formData, setFormData] = useState<Appointment>({
|
||||
patient: '',
|
||||
time: '',
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (appointment) {
|
||||
setFormData(appointment);
|
||||
} else {
|
||||
setFormData({
|
||||
patient: '',
|
||||
time: '',
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: professionals[0]?.id || '',
|
||||
notes: ''
|
||||
});
|
||||
}
|
||||
}, [appointment, professionals]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{appointment ? 'Editar Agendamento' : 'Novo Agendamento'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Paciente
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="patient"
|
||||
value={formData.patient}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Profissional
|
||||
</label>
|
||||
<select
|
||||
name="professional"
|
||||
value={formData.professional}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="">Selecione um profissional</option>
|
||||
{professionals.map(prof => (
|
||||
<option key={prof.id} value={prof.id}>
|
||||
{prof.name} - {prof.specialty}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data e Hora
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="time"
|
||||
value={formData.time}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Duração (min)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="duration"
|
||||
value={formData.duration}
|
||||
onChange={handleChange}
|
||||
min="15"
|
||||
step="15"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo
|
||||
</label>
|
||||
<select
|
||||
name="type"
|
||||
value={formData.type}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="consulta">Consulta</option>
|
||||
<option value="exame">Exame</option>
|
||||
<option value="retorno">Retorno</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="pending">Pendente</option>
|
||||
<option value="confirmed">Confirmado</option>
|
||||
<option value="absent">Ausente</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observações
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes || ''}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Salvar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
144
susconecta/components/agendamento/ListaEspera.tsx
Normal file
144
susconecta/components/agendamento/ListaEspera.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Bell, Plus } from 'lucide-react';
|
||||
|
||||
interface WaitingPatient {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
preferredDate: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
contact: string;
|
||||
}
|
||||
|
||||
interface ListaEsperaProps {
|
||||
patients: WaitingPatient[];
|
||||
onNotify: (patientId: string) => void;
|
||||
onAddToWaitlist: () => void;
|
||||
}
|
||||
|
||||
export default function ListaEspera({ patients, onNotify, onAddToWaitlist }: ListaEsperaProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredPatients = patients.filter(patient =>
|
||||
patient.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
patient.specialty.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const getPriorityLabel = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'Alta';
|
||||
case 'medium': return 'Média';
|
||||
case 'low': return 'Baixa';
|
||||
default: return priority;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'bg-red-100 text-red-800';
|
||||
case 'medium': return 'bg-yellow-100 text-yellow-800';
|
||||
case 'low': return 'bg-green-100 text-green-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">Lista de Espera Inteligente</h2>
|
||||
<button
|
||||
onClick={onAddToWaitlist}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Adicionar à Lista
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500">🔍</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar paciente..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Paciente
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Especialidade
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Data Preferencial
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Prioridade
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Contato
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Ações
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredPatients.map((patient) => (
|
||||
<tr key={patient.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{patient.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{patient.specialty}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(patient.preferredDate).toLocaleDateString('pt-BR')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getPriorityColor(patient.priority)}`}>
|
||||
{getPriorityLabel(patient.priority)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{patient.contact}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => onNotify(patient.id)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-3"
|
||||
title="Notificar paciente"
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{filteredPatients.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Nenhum paciente encontrado na lista de espera
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
susconecta/components/agendamento/index.ts
Normal file
4
susconecta/components/agendamento/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// components/agendamento/index.ts
|
||||
export { default as AgendaCalendar } from './AgendaCalendar';
|
||||
export { default as AppointmentModal } from './AppointmentModal';
|
||||
export { default as ListaEspera } from './ListaEspera';
|
||||
@ -3,12 +3,13 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stethoscope } from "lucide-react"
|
||||
import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stethoscope, User } from "lucide-react"
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/dashboard", icon: Home },
|
||||
{ name: "Agenda", href: "/dashboard/agenda", icon: Calendar },
|
||||
{ name: "Agendamento", href: "/agendamento", icon: Calendar },
|
||||
{ name: "Pacientes", href: "/dashboard/pacientes", icon: Users },
|
||||
{ name: "Médicos", href: "/dashboard/medicos", icon: User },
|
||||
{ name: "Consultas", href: "/dashboard/consultas", icon: UserCheck },
|
||||
{ name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText },
|
||||
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
|
||||
|
||||
@ -12,10 +12,10 @@ export function Footer() {
|
||||
<footer className="bg-background border-t border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
||||
{/* Copyright */}
|
||||
{}
|
||||
<div className="text-muted-foreground text-sm">© 2025 SUS Conecta</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
{}
|
||||
<nav className="flex items-center space-x-8">
|
||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||
Termos
|
||||
@ -28,7 +28,7 @@ export function Footer() {
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{/* Back to Top Button */}
|
||||
{}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
616
susconecta/components/forms/patient-registration-form.tsx
Normal file
616
susconecta/components/forms/patient-registration-form.tsx
Normal file
@ -0,0 +1,616 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
|
||||
|
||||
import {
|
||||
Paciente,
|
||||
PacienteInput,
|
||||
buscarCepAPI,
|
||||
validarCPF,
|
||||
criarPaciente,
|
||||
atualizarPaciente,
|
||||
uploadFotoPaciente,
|
||||
removerFotoPaciente,
|
||||
adicionarAnexo,
|
||||
listarAnexos,
|
||||
removerAnexo,
|
||||
buscarPacientePorId,
|
||||
} from "@/lib/api";
|
||||
|
||||
type Mode = "create" | "edit";
|
||||
|
||||
export interface PatientRegistrationFormProps {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
patientId?: number | null;
|
||||
inline?: boolean;
|
||||
mode?: Mode;
|
||||
onSaved?: (paciente: Paciente) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
photo: File | null;
|
||||
nome: string;
|
||||
nome_social: string;
|
||||
cpf: string;
|
||||
rg: string;
|
||||
sexo: string;
|
||||
data_nascimento: string;
|
||||
email: string;
|
||||
telefone: string;
|
||||
cep: string;
|
||||
logradouro: string;
|
||||
numero: string;
|
||||
complemento: string;
|
||||
bairro: string;
|
||||
cidade: string;
|
||||
estado: string;
|
||||
observacoes: string;
|
||||
anexos: File[];
|
||||
};
|
||||
|
||||
const initial: FormData = {
|
||||
photo: null,
|
||||
nome: "",
|
||||
nome_social: "",
|
||||
cpf: "",
|
||||
rg: "",
|
||||
sexo: "",
|
||||
data_nascimento: "",
|
||||
email: "",
|
||||
telefone: "",
|
||||
cep: "",
|
||||
logradouro: "",
|
||||
numero: "",
|
||||
complemento: "",
|
||||
bairro: "",
|
||||
cidade: "",
|
||||
estado: "",
|
||||
observacoes: "",
|
||||
anexos: [],
|
||||
};
|
||||
|
||||
export function PatientRegistrationForm({
|
||||
open = true,
|
||||
onOpenChange,
|
||||
patientId = null,
|
||||
inline = false,
|
||||
mode = "create",
|
||||
onSaved,
|
||||
onClose,
|
||||
}: PatientRegistrationFormProps) {
|
||||
const [form, setForm] = useState<FormData>(initial);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||
|
||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (mode !== "edit" || patientId == null) return;
|
||||
try {
|
||||
const p = await buscarPacientePorId(String(patientId));
|
||||
setForm((s) => ({
|
||||
...s,
|
||||
nome: p.nome || "",
|
||||
nome_social: p.nome_social || "",
|
||||
cpf: p.cpf || "",
|
||||
rg: p.rg || "",
|
||||
sexo: p.sexo || "",
|
||||
data_nascimento: (p.data_nascimento as string) || "",
|
||||
telefone: p.telefone || "",
|
||||
email: p.email || "",
|
||||
cep: p.endereco?.cep || "",
|
||||
logradouro: p.endereco?.logradouro || "",
|
||||
numero: p.endereco?.numero || "",
|
||||
complemento: p.endereco?.complemento || "",
|
||||
bairro: p.endereco?.bairro || "",
|
||||
cidade: p.endereco?.cidade || "",
|
||||
estado: p.endereco?.estado || "",
|
||||
observacoes: p.observacoes || "",
|
||||
}));
|
||||
const ax = await listarAnexos(String(patientId)).catch(() => []);
|
||||
setServerAnexos(Array.isArray(ax) ? ax : []);
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [mode, patientId]);
|
||||
|
||||
function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
|
||||
setForm((s) => ({ ...s, [k]: v }));
|
||||
if (errors[k as string]) setErrors((e) => ({ ...e, [k]: "" }));
|
||||
}
|
||||
|
||||
function formatCPF(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 11);
|
||||
return n.replace(/(\d{3})(\d{3})(\d{3})(\d{0,2})/, (_, a, b, c, d) => `${a}.${b}.${c}${d ? "-" + d : ""}`);
|
||||
}
|
||||
function handleCPFChange(v: string) {
|
||||
setField("cpf", formatCPF(v));
|
||||
}
|
||||
|
||||
function formatCEP(v: string) {
|
||||
const n = v.replace(/\D/g, "").slice(0, 8);
|
||||
return n.replace(/(\d{5})(\d{0,3})/, (_, a, b) => `${a}${b ? "-" + b : ""}`);
|
||||
}
|
||||
async function fillFromCEP(cep: string) {
|
||||
const clean = cep.replace(/\D/g, "");
|
||||
if (clean.length !== 8) return;
|
||||
setSearchingCEP(true);
|
||||
try {
|
||||
const res = await buscarCepAPI(clean);
|
||||
if (res?.erro) {
|
||||
setErrors((e) => ({ ...e, cep: "CEP não encontrado" }));
|
||||
} else {
|
||||
setField("logradouro", res.logradouro ?? "");
|
||||
setField("bairro", res.bairro ?? "");
|
||||
setField("cidade", res.localidade ?? "");
|
||||
setField("estado", res.uf ?? "");
|
||||
}
|
||||
} catch {
|
||||
setErrors((e) => ({ ...e, cep: "Erro ao buscar CEP" }));
|
||||
} finally {
|
||||
setSearchingCEP(false);
|
||||
}
|
||||
}
|
||||
|
||||
function validateLocal(): boolean {
|
||||
const e: Record<string, string> = {};
|
||||
if (!form.nome.trim()) e.nome = "Nome é obrigatório";
|
||||
if (!form.cpf.trim()) e.cpf = "CPF é obrigatório";
|
||||
setErrors(e);
|
||||
return Object.keys(e).length === 0;
|
||||
}
|
||||
|
||||
function toPayload(): PacienteInput {
|
||||
return {
|
||||
nome: form.nome,
|
||||
nome_social: form.nome_social || null,
|
||||
cpf: form.cpf,
|
||||
rg: form.rg || null,
|
||||
sexo: form.sexo || null,
|
||||
data_nascimento: form.data_nascimento || null,
|
||||
telefone: form.telefone || null,
|
||||
email: form.email || null,
|
||||
endereco: {
|
||||
cep: form.cep || null,
|
||||
logradouro: form.logradouro || null,
|
||||
numero: form.numero || null,
|
||||
complemento: form.complemento || null,
|
||||
bairro: form.bairro || null,
|
||||
cidade: form.cidade || null,
|
||||
estado: form.estado || null,
|
||||
},
|
||||
observacoes: form.observacoes || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSubmit(ev: React.FormEvent) {
|
||||
ev.preventDefault();
|
||||
if (!validateLocal()) return;
|
||||
|
||||
|
||||
try {
|
||||
const { valido, existe } = await validarCPF(form.cpf);
|
||||
if (!valido) {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF inválido (validação externa)" }));
|
||||
return;
|
||||
}
|
||||
if (existe && mode === "create") {
|
||||
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = toPayload();
|
||||
|
||||
let saved: Paciente;
|
||||
if (mode === "create") {
|
||||
saved = await criarPaciente(payload);
|
||||
} else {
|
||||
if (patientId == null) throw new Error("Paciente inexistente para edição");
|
||||
saved = await atualizarPaciente(String(patientId), payload);
|
||||
}
|
||||
|
||||
if (form.photo && saved?.id) {
|
||||
try {
|
||||
await uploadFotoPaciente(saved.id, form.photo);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (form.anexos.length && saved?.id) {
|
||||
for (const f of form.anexos) {
|
||||
try {
|
||||
await adicionarAnexo(saved.id, f);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
onSaved?.(saved);
|
||||
setForm(initial);
|
||||
setPhotoPreview(null);
|
||||
setServerAnexos([]);
|
||||
|
||||
if (inline) onClose?.();
|
||||
else onOpenChange?.(false);
|
||||
|
||||
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
|
||||
} catch (err: any) {
|
||||
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePhoto(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
if (f.size > 5 * 1024 * 1024) {
|
||||
setErrors((e) => ({ ...e, photo: "Arquivo muito grande. Máx 5MB." }));
|
||||
return;
|
||||
}
|
||||
setField("photo", f);
|
||||
const fr = new FileReader();
|
||||
fr.onload = (ev) => setPhotoPreview(String(ev.target?.result || ""));
|
||||
fr.readAsDataURL(f);
|
||||
}
|
||||
|
||||
function addLocalAnexos(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const fs = Array.from(e.target.files || []);
|
||||
setField("anexos", [...form.anexos, ...fs]);
|
||||
}
|
||||
function removeLocalAnexo(idx: number) {
|
||||
const clone = [...form.anexos];
|
||||
clone.splice(idx, 1);
|
||||
setField("anexos", clone);
|
||||
}
|
||||
|
||||
async function handleRemoverFotoServidor() {
|
||||
if (mode !== "edit" || !patientId) return;
|
||||
try {
|
||||
await removerFotoPaciente(String(patientId));
|
||||
alert("Foto removida.");
|
||||
} catch (e: any) {
|
||||
alert(e?.message || "Não foi possível remover a foto.");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoverAnexoServidor(anexoId: string | number) {
|
||||
if (mode !== "edit" || !patientId) return;
|
||||
try {
|
||||
await removerAnexo(String(patientId), anexoId);
|
||||
setServerAnexos((prev) => prev.filter((a) => String(a.id ?? a.anexo_id) !== String(anexoId)));
|
||||
} catch (e: any) {
|
||||
alert(e?.message || "Não foi possível remover o anexo.");
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{errors.submit && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{errors.submit}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{}
|
||||
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
Dados Pessoais
|
||||
</span>
|
||||
{expanded.dados ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
|
||||
{photoPreview ? (
|
||||
|
||||
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="photo" className="cursor-pointer">
|
||||
<Button type="button" variant="outline" asChild>
|
||||
<span>
|
||||
<Upload className="mr-2 h-4 w-4" /> Carregar Foto
|
||||
</span>
|
||||
</Button>
|
||||
</Label>
|
||||
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhoto} />
|
||||
{mode === "edit" && (
|
||||
<Button type="button" variant="ghost" onClick={handleRemoverFotoServidor}>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> Remover foto
|
||||
</Button>
|
||||
)}
|
||||
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
|
||||
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome *</Label>
|
||||
<Input value={form.nome} onChange={(e) => setField("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
|
||||
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Nome Social</Label>
|
||||
<Input value={form.nome_social} onChange={(e) => setField("nome_social", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>CPF *</Label>
|
||||
<Input
|
||||
value={form.cpf}
|
||||
onChange={(e) => handleCPFChange(e.target.value)}
|
||||
placeholder="000.000.000-00"
|
||||
maxLength={14}
|
||||
className={errors.cpf ? "border-destructive" : ""}
|
||||
/>
|
||||
{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>RG</Label>
|
||||
<Input value={form.rg} onChange={(e) => setField("rg", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Sexo</Label>
|
||||
<RadioGroup value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="masculino" id="masculino" />
|
||||
<Label htmlFor="masculino">Masculino</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="feminino" id="feminino" />
|
||||
<Label htmlFor="feminino">Feminino</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="outro" id="outro" />
|
||||
<Label htmlFor="outro">Outro</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Data de Nascimento</Label>
|
||||
<Input type="date" value={form.data_nascimento} onChange={(e) => setField("data_nascimento", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Contato</span>
|
||||
{expanded.contato ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>E-mail</Label>
|
||||
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Telefone</Label>
|
||||
<Input value={form.telefone} onChange={(e) => setField("telefone", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Endereço</span>
|
||||
{expanded.endereco ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>CEP</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={form.cep}
|
||||
onChange={(e) => {
|
||||
const v = formatCEP(e.target.value);
|
||||
setField("cep", v);
|
||||
if (v.replace(/\D/g, "").length === 8) fillFromCEP(v);
|
||||
}}
|
||||
placeholder="00000-000"
|
||||
maxLength={9}
|
||||
disabled={isSearchingCEP}
|
||||
className={errors.cep ? "border-destructive" : ""}
|
||||
/>
|
||||
{isSearchingCEP && <Loader2 className="absolute right-3 top-3 h-4 w-4 animate-spin" />}
|
||||
</div>
|
||||
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Logradouro</Label>
|
||||
<Input value={form.logradouro} onChange={(e) => setField("logradouro", e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Número</Label>
|
||||
<Input value={form.numero} onChange={(e) => setField("numero", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Complemento</Label>
|
||||
<Input value={form.complemento} onChange={(e) => setField("complemento", e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Bairro</Label>
|
||||
<Input value={form.bairro} onChange={(e) => setField("bairro", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Cidade</Label>
|
||||
<Input value={form.cidade} onChange={(e) => setField("cidade", e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Estado</Label>
|
||||
<Input value={form.estado} onChange={(e) => setField("estado", e.target.value)} placeholder="UF" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
|
||||
<Card>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Observações e Anexos</span>
|
||||
{expanded.obs ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Observações</Label>
|
||||
<Textarea rows={4} value={form.observacoes} onChange={(e) => setField("observacoes", e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Adicionar anexos</Label>
|
||||
<div className="border-2 border-dashed rounded-lg p-4">
|
||||
<Label htmlFor="anexos" className="cursor-pointer">
|
||||
<div className="text-center">
|
||||
<Upload className="mx-auto h-7 w-7 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">Clique para adicionar documentos (PDF, imagens, etc.)</p>
|
||||
</div>
|
||||
</Label>
|
||||
<Input id="anexos" type="file" multiple className="hidden" onChange={addLocalAnexos} />
|
||||
</div>
|
||||
|
||||
{form.anexos.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{form.anexos.map((f, i) => (
|
||||
<div key={`${f.name}-${i}`} className="flex items-center justify-between p-2 border rounded">
|
||||
<span className="text-sm">{f.name}</span>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => removeLocalAnexo(i)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mode === "edit" && serverAnexos.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>Anexos já enviados</Label>
|
||||
<div className="space-y-2">
|
||||
{serverAnexos.map((ax) => {
|
||||
const id = ax.id ?? ax.anexo_id ?? ax.uuid ?? "";
|
||||
return (
|
||||
<div key={String(id)} className="flex items-center justify-between p-2 border rounded">
|
||||
<span className="text-sm">{ax.nome || ax.filename || `Anexo ${id}`}</span>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => handleRemoverAnexoServidor(String(id))}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
|
||||
{}
|
||||
<div className="flex justify-end gap-4 pt-6 border-t">
|
||||
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
{isSubmitting ? "Salvando..." : mode === "create" ? "Salvar Paciente" : "Atualizar Paciente"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
if (inline) return <div className="space-y-6">{content}</div>;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" /> {title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -19,7 +19,7 @@ export function Header() {
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
{}
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
<Link
|
||||
href="/"
|
||||
@ -32,7 +32,7 @@ export function Header() {
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Desktop Action Buttons */}
|
||||
{}
|
||||
<div className="hidden md:flex items-center space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -40,11 +40,9 @@ export function Header() {
|
||||
>
|
||||
Sou Paciente
|
||||
</Button>
|
||||
<Link href="/dashboard">
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
Sou Profissional de Saúde
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
<Link href="/profissional">Sou Profissional de Saúde</Link>
|
||||
</Button>
|
||||
<Link href="/dashboard">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -55,13 +53,13 @@ export function Header() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
{}
|
||||
<button className="md:hidden p-2" onClick={() => setIsMenuOpen(!isMenuOpen)} aria-label="Toggle menu">
|
||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{}
|
||||
{isMenuOpen && (
|
||||
<div className="md:hidden py-4 border-t border-border">
|
||||
<nav className="flex flex-col space-y-4">
|
||||
@ -86,11 +84,9 @@ export function Header() {
|
||||
>
|
||||
Sou Paciente
|
||||
</Button>
|
||||
<Link href="/dashboard">
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
|
||||
Sou Profissional de Saúde
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
|
||||
Sou Profissional de Saúde
|
||||
</Button>
|
||||
<Link href="/dashboard">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Shield, Clock, Users } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="py-16 lg:py-24 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Content */}
|
||||
{}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
||||
@ -22,7 +23,7 @@ export function HeroSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
Sou Paciente
|
||||
@ -32,12 +33,12 @@ export function HeroSection() {
|
||||
variant="outline"
|
||||
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
|
||||
>
|
||||
Sou Profissional de Saúde
|
||||
<Link href="/profissional">Sou Profissional de Saúde</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Image */}
|
||||
{}
|
||||
<div className="relative">
|
||||
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-8">
|
||||
<img
|
||||
@ -49,7 +50,7 @@ export function HeroSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
{}
|
||||
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
|
||||
@ -698,6 +698,7 @@ function SidebarMenuSubButton({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
||||
95
susconecta/hooks/UseAgenda.ts
Normal file
95
susconecta/hooks/UseAgenda.ts
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export interface Appointment {
|
||||
id: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
professional: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Professional {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
export interface WaitingPatient {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
preferredDate: string;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
contact: string;
|
||||
}
|
||||
|
||||
export const useAgenda = () => {
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [waitingList, setWaitingList] = useState<WaitingPatient[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
|
||||
const [isWaitlistModalOpen, setIsWaitlistModalOpen] = useState(false);
|
||||
|
||||
const professionals: Professional[] = [
|
||||
{ id: '1', name: 'Dr. Carlos Silva', specialty: 'Cardiologia' },
|
||||
{ id: '2', name: 'Dra. Maria Santos', specialty: 'Dermatologia' },
|
||||
{ id: '3', name: 'Dr. João Oliveira', specialty: 'Ortopedia' },
|
||||
];
|
||||
|
||||
const handleSaveAppointment = (appointment: Appointment) => {
|
||||
if (appointment.id) {
|
||||
|
||||
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
|
||||
} else {
|
||||
|
||||
const newAppointment = {
|
||||
...appointment,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
setAppointments(prev => [...prev, newAppointment]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAppointment = (appointment: Appointment) => {
|
||||
setSelectedAppointment(appointment);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAddAppointment = () => {
|
||||
setSelectedAppointment(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedAppointment(null);
|
||||
};
|
||||
|
||||
const handleNotifyPatient = (patientId: string) => {
|
||||
|
||||
console.log(`Notificando paciente ${patientId}`);
|
||||
};
|
||||
|
||||
const handleAddToWaitlist = () => {
|
||||
setIsWaitlistModalOpen(true);
|
||||
};
|
||||
|
||||
return {
|
||||
appointments,
|
||||
waitingList,
|
||||
professionals,
|
||||
isModalOpen,
|
||||
selectedAppointment,
|
||||
isWaitlistModalOpen,
|
||||
handleSaveAppointment,
|
||||
handleEditAppointment,
|
||||
handleAddAppointment,
|
||||
handleCloseModal,
|
||||
handleNotifyPatient,
|
||||
handleAddToWaitlist,
|
||||
};
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
@ -93,8 +93,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
|
||||
252
susconecta/lib/api.ts
Normal file
252
susconecta/lib/api.ts
Normal file
@ -0,0 +1,252 @@
|
||||
|
||||
|
||||
export type ApiOk<T = any> = {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
pagination?: {
|
||||
current_page?: number;
|
||||
per_page?: number;
|
||||
total_pages?: number;
|
||||
total?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type Endereco = {
|
||||
cep?: string;
|
||||
logradouro?: string;
|
||||
numero?: string;
|
||||
complemento?: string;
|
||||
bairro?: string;
|
||||
cidade?: string;
|
||||
estado?: string;
|
||||
};
|
||||
|
||||
export type Paciente = {
|
||||
id: string;
|
||||
nome?: string;
|
||||
nome_social?: string | null;
|
||||
cpf?: string;
|
||||
rg?: string | null;
|
||||
sexo?: string | null;
|
||||
data_nascimento?: string | null;
|
||||
telefone?: string;
|
||||
email?: string;
|
||||
endereco?: Endereco;
|
||||
observacoes?: string | null;
|
||||
foto_url?: string | null;
|
||||
};
|
||||
|
||||
export type PacienteInput = {
|
||||
nome: string;
|
||||
nome_social?: string | null;
|
||||
cpf: string;
|
||||
rg?: string | null;
|
||||
sexo?: string | null;
|
||||
data_nascimento?: string | null;
|
||||
telefone?: string | null;
|
||||
email?: string | null;
|
||||
endereco?: {
|
||||
cep?: string | null;
|
||||
logradouro?: string | null;
|
||||
numero?: string | null;
|
||||
complemento?: string | null;
|
||||
bairro?: string | null;
|
||||
cidade?: string | null;
|
||||
estado?: string | null;
|
||||
};
|
||||
observacoes?: string | null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "https://mock.apidog.com/m1/1053378-0-default";
|
||||
|
||||
const PATHS = {
|
||||
pacientes: "/pacientes",
|
||||
pacienteId: (id: string | number) => `/pacientes/${id}`,
|
||||
foto: (id: string | number) => `/pacientes/${id}/foto`,
|
||||
anexos: (id: string | number) => `/pacientes/${id}/anexos`,
|
||||
anexoId: (id: string | number, anexoId: string | number) => `/pacientes/${id}/anexos/${anexoId}`,
|
||||
validarCPF: "/pacientes/validar-cpf",
|
||||
cep: (cep: string) => `/utils/cep/${cep}`,
|
||||
} as const;
|
||||
|
||||
function headers(kind: "json" | "form" = "json"): Record<string, string> {
|
||||
const h: Record<string, string> = {};
|
||||
const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim();
|
||||
if (token) h.Authorization = `Bearer ${token}`;
|
||||
if (kind === "json") h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
|
||||
function logAPI(title: string, info: { url?: string; payload?: any; result?: any } = {}) {
|
||||
try {
|
||||
console.group(`[API] ${title}`);
|
||||
if (info.url) console.log("url:", info.url);
|
||||
if (info.payload !== undefined) console.log("payload:", info.payload);
|
||||
if (info.result !== undefined) console.log("API result:", info.result);
|
||||
console.groupEnd();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function parse<T>(res: Response): Promise<T> {
|
||||
let json: any = null;
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
|
||||
}
|
||||
if (!res.ok) {
|
||||
const code = json?.apidogError?.code ?? res.status;
|
||||
const msg = json?.apidogError?.message ?? res.statusText;
|
||||
throw new Error(`${code}: ${msg}`);
|
||||
}
|
||||
|
||||
return (json?.data ?? json) as T;
|
||||
}
|
||||
|
||||
//
|
||||
// Pacientes (CRUD)
|
||||
//
|
||||
export async function listarPacientes(params?: { page?: number; limit?: number; q?: string }): Promise<Paciente[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.page) query.set("page", String(params.page));
|
||||
if (params?.limit) query.set("limit", String(params.limit));
|
||||
if (params?.q) query.set("q", params.q);
|
||||
const url = `${API_BASE}${PATHS.pacientes}${query.toString() ? `?${query.toString()}` : ""}`;
|
||||
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Paciente[]>>(res);
|
||||
logAPI("listarPacientes", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("buscarPacientePorId", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacientes}`;
|
||||
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("criarPaciente", { url, payload: input, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "PUT", headers: headers("json"), body: JSON.stringify(input) });
|
||||
const data = await parse<ApiOk<Paciente>>(res);
|
||||
logAPI("atualizarPaciente", { url, payload: input, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function excluirPaciente(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.pacienteId(id)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("excluirPaciente", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Foto
|
||||
//
|
||||
|
||||
export async function uploadFotoPaciente(id: string | number, file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> {
|
||||
const url = `${API_BASE}${PATHS.foto(id)}`;
|
||||
const fd = new FormData();
|
||||
// nome de campo mais comum no mock
|
||||
fd.append("foto", file);
|
||||
const res = await fetch(url, { method: "POST", headers: headers("form"), body: fd });
|
||||
const data = await parse<ApiOk<{ foto_url?: string; thumbnail_url?: string }>>(res);
|
||||
logAPI("uploadFotoPaciente", { url, payload: { file: file.name }, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function removerFotoPaciente(id: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.foto(id)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("removerFotoPaciente", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Anexos
|
||||
//
|
||||
|
||||
export async function listarAnexos(id: string | number): Promise<any[]> {
|
||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||
const res = await fetch(url, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<ApiOk<any[]>>(res);
|
||||
logAPI("listarAnexos", { url, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function adicionarAnexo(id: string | number, file: File): Promise<any> {
|
||||
const url = `${API_BASE}${PATHS.anexos(id)}`;
|
||||
const fd = new FormData();
|
||||
|
||||
fd.append("arquivo", file);
|
||||
const res = await fetch(url, { method: "POST", body: fd, headers: headers("form") });
|
||||
const data = await parse<ApiOk<any>>(res);
|
||||
logAPI("adicionarAnexo", { url, payload: { file: file.name }, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function removerAnexo(id: string | number, anexoId: string | number): Promise<void> {
|
||||
const url = `${API_BASE}${PATHS.anexoId(id, anexoId)}`;
|
||||
const res = await fetch(url, { method: "DELETE", headers: headers("json") });
|
||||
await parse<any>(res);
|
||||
logAPI("removerAnexo", { url, result: { ok: true } });
|
||||
}
|
||||
|
||||
//
|
||||
// Validações
|
||||
//
|
||||
|
||||
export async function validarCPF(cpf: string): Promise<{ valido: boolean; existe: boolean; paciente_id: string | null }> {
|
||||
const url = `${API_BASE}${PATHS.validarCPF}`;
|
||||
const payload = { cpf };
|
||||
const res = await fetch(url, { method: "POST", headers: headers("json"), body: JSON.stringify(payload) });
|
||||
const data = await parse<ApiOk<{ valido: boolean; existe: boolean; paciente_id: string | null }>>(res);
|
||||
logAPI("validarCPF", { url, payload, result: data });
|
||||
return data?.data ?? (data as any);
|
||||
}
|
||||
|
||||
export async function buscarCepAPI(cep: string): Promise<{ logradouro?: string; bairro?: string; localidade?: string; uf?: string; erro?: boolean }> {
|
||||
const clean = (cep || "").replace(/\D/g, "");
|
||||
const urlMock = `${API_BASE}${PATHS.cep(clean)}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(urlMock, { method: "GET", headers: headers("json") });
|
||||
const data = await parse<any>(res); // pode vir direto ou dentro de {data}
|
||||
logAPI("buscarCEP (mock)", { url: urlMock, payload: { cep: clean }, result: data });
|
||||
const d = data?.data ?? data ?? {};
|
||||
return {
|
||||
logradouro: d.logradouro ?? d.street ?? "",
|
||||
bairro: d.bairro ?? d.neighborhood ?? "",
|
||||
localidade: d.localidade ?? d.city ?? "",
|
||||
uf: d.uf ?? d.state ?? "",
|
||||
erro: false,
|
||||
};
|
||||
} catch {
|
||||
// fallback ViaCEP
|
||||
const urlVia = `https://viacep.com.br/ws/${clean}/json/`;
|
||||
const resV = await fetch(urlVia);
|
||||
const jsonV = await resV.json().catch(() => ({}));
|
||||
logAPI("buscarCEP (ViaCEP/fallback)", { url: urlVia, payload: { cep: clean }, result: jsonV });
|
||||
if (jsonV?.erro) return { erro: true };
|
||||
return {
|
||||
logradouro: jsonV.logradouro ?? "",
|
||||
bairro: jsonV.bairro ?? "",
|
||||
localidade: jsonV.localidade ?? "",
|
||||
uf: jsonV.uf ?? "",
|
||||
erro: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
778
susconecta/package-lock.json
generated
778
susconecta/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,11 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.19",
|
||||
"@fullcalendar/daygrid": "^6.1.19",
|
||||
"@fullcalendar/interaction": "^6.1.19",
|
||||
"@fullcalendar/react": "^6.1.19",
|
||||
"@fullcalendar/timegrid": "^6.1.19",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "latest",
|
||||
"@radix-ui/react-alert-dialog": "latest",
|
||||
@ -47,11 +52,11 @@
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "latest",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.2.4",
|
||||
"next": "14.2.16",
|
||||
"next-themes": "latest",
|
||||
"react": "^19",
|
||||
"react": "^18",
|
||||
"react-day-picker": "latest",
|
||||
"react-dom": "^19",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "latest",
|
||||
"react-resizable-panels": "latest",
|
||||
"recharts": "latest",
|
||||
@ -64,11 +69,11 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: var(--primary)
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
@ -74,7 +74,7 @@
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
:root {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-background: var(--background);
|
||||
@ -117,9 +117,17 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
border-color: var(--border);
|
||||
outline-color: var(--ring);
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-offset: 0.5px;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
.buttonText {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
@ -22,6 +22,6 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "lib/api.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user