diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d570088 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fe0f642 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,625 @@ +{ + "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", + "react-signature-canvas": "^1.1.0-alpha.2" + } + }, + "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/signature_pad": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/signature_pad/-/signature_pad-2.3.6.tgz", + "integrity": "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ==", + "license": "MIT" + }, + "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/react-signature-canvas": { + "version": "1.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.1.0-alpha.2.tgz", + "integrity": "sha512-tKUNk3Gmh04Ug4K8p5g8Is08BFUKvbXxi0PyetQ/f8OgCBzcx4vqNf9+OArY/TdNdfHtswXQNRwZD6tyELjkjQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.17.9", + "@types/signature_pad": "^2.3.0", + "signature_pad": "^2.3.2", + "trim-canvas": "^0.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/agilgur5" + }, + "peerDependencies": { + "@types/prop-types": "^15.7.3", + "@types/react": "0.14 - 19", + "prop-types": "^15.5.8", + "react": "0.14 - 19", + "react-dom": "0.14 - 19" + }, + "peerDependenciesMeta": { + "@types/prop-types": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "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/signature_pad": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz", + "integrity": "sha512-peYXLxOsIY6MES2TrRLDiNg2T++8gGbpP2yaC+6Ohtxr+a2dzoaqWosWDY9sWqTAAk6E/TyQO+LJw9zQwyu5kA==", + "license": "MIT" + }, + "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/trim-canvas": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/trim-canvas/-/trim-canvas-0.1.2.tgz", + "integrity": "sha512-nd4Ga3iLFV94mdhW9JFMLpQbHUyCQuhFOD71PEAt1NjtMD5wbZctzhX8c3agHNybMR5zXD1XTGoIEWk995E6pQ==", + "license": "Apache-2.0" + }, + "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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e0ec9ff --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@headlessui/react": "^2.2.7", + "@heroicons/react": "^2.2.0", + "date-fns": "^4.1.0", + "react-big-calendar": "^1.19.4", + "react-signature-canvas": "^1.1.0-alpha.2" + } +} diff --git a/susconecta/.gitignore b/susconecta/.gitignore new file mode 100644 index 0000000..29879dc --- /dev/null +++ b/susconecta/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.tsriseup-squad20/ +susconecta/riseup-squad20/ +riseup-squad20/ diff --git a/susconecta/app/(main-routes)/calendar/index.css b/susconecta/app/(main-routes)/calendar/index.css new file mode 100644 index 0000000..c631baa --- /dev/null +++ b/susconecta/app/(main-routes)/calendar/index.css @@ -0,0 +1,41 @@ + +.fc-media-screen { + flex-grow: 1; + height: 74vh; +} + + +.fc-prev-button, +.fc-next-button { + background-color: var(--color-blue-600) !important; + border: none !important; + transition: 0.2s ease; +} + +.fc-prev-button:hover, +.fc-next-button:hover { + background-color: var(--color-blue-700) !important; +} + +.fc-timeGridWeek-button, +.fc-timeGridDay-button, +.fc-dayGridMonth-button { + border: none !important; + background-color: var(--color-blue-600) !important; + transition: 0.2s ease; +} + +.fc-timeGridWeek-button:hover, +.fc-timeGridDay-button:hover, +.fc-dayGridMonth-button:hover { + background-color: var(--color-blue-700) !important; +} + +.fc-button-active { + background-color: var(--color-blue-500) !important; +} + +.fc-toolbar-title { + font-weight: bold; + color: var(--color-gray-900); +} \ No newline at end of file diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx new file mode 100644 index 0000000..dabcec7 --- /dev/null +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useEffect, useState } from "react"; +import dynamic from "next/dynamic"; +import pt_br_locale from "@fullcalendar/core/locales/pt-br"; +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import { EventInput } from "@fullcalendar/core/index.js"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { PagesHeader } from "@/components/dashboard/header"; +import { Button } from "@/components/ui/button"; +import { + mockAppointments, + mockWaitingList, +} from "@/lib/mocks/appointment-mocks"; +import "./index.css"; +import Link from "next/link"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +const ListaEspera = dynamic( + () => import("@/components/agendamento/ListaEspera"), + { ssr: false } +); + +export default function AgendamentoPage() { + const [appointments, setAppointments] = useState(mockAppointments); + const [waitingList, setWaitingList] = useState(mockWaitingList); + const [activeTab, setActiveTab] = useState<"calendar" | "espera">("calendar"); + const [requestsList, setRequestsList] = useState(); + + useEffect(() => { + document.addEventListener("keydown", (event) => { + if (event.key === "c") { + setActiveTab("calendar"); + } + if (event.key === "f") { + setActiveTab("espera"); + } + }); + }, []); + + useEffect(() => { + let events: EventInput[] = []; + appointments.forEach((obj) => { + const event: EventInput = { + title: `${obj.patient}: ${obj.type}`, + start: new Date(obj.time), + end: new Date(new Date(obj.time).getTime() + obj.duration * 60 * 1000), + color: + obj.status === "confirmed" + ? "#68d68a" + : obj.status === "pending" + ? "#ffe55f" + : "#ff5f5fff", + }; + events.push(event); + }); + setRequestsList(events); + }, [appointments]); + + // mantive para caso a lógica de salvar consulta passe a funcionar + 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 handleNotifyPatient = (patientId: string) => { + console.log(`Notificando paciente ${patientId}`); + }; + + return ( +
+
+
+
+
+

{activeTab === "calendar" ? "Calendário" : "Lista de Espera"}

+

+ Navegue através dos atalhos: Calendário (C) ou Fila de espera + (F). +

+
+
+ {/* + + */} + + + Opções » + + + + Agendamento + + + Procedimento + + + Financeiro + + + + +
+ + + +
+
+
+ + {activeTab === "calendar" ? ( +
+ { + info.view.calendar.changeView("timeGridDay", info.dateStr); + }} + selectable={true} + selectMirror={true} + dayMaxEvents={true} + dayMaxEventRows={3} + /> +
+ ) : ( + {}} + /> + )} +
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx new file mode 100644 index 0000000..c55cdac --- /dev/null +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -0,0 +1,362 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { + MoreHorizontal, + PlusCircle, + Search, + Eye, + Edit, + Trash2, + ArrowLeft, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks"; +import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; + + +const formatDate = (date: string | Date) => { + if (!date) return ""; + return new Date(date).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +const capitalize = (s: string) => { + if (typeof s !== 'string' || s.length === 0) return ''; + return s.charAt(0).toUpperCase() + s.slice(1); +}; + +export default function ConsultasPage() { + const [appointments, setAppointments] = useState(mockAppointments); + const [showForm, setShowForm] = useState(false); + const [editingAppointment, setEditingAppointment] = useState(null); + const [viewingAppointment, setViewingAppointment] = useState(null); + + const mapAppointmentToFormData = (appointment: any) => { + const professional = mockProfessionals.find(p => p.id === appointment.professional); + const appointmentDate = new Date(appointment.time); + + return { + id: appointment.id, + patientName: appointment.patient, + professionalName: professional ? professional.name : '', + appointmentDate: appointmentDate.toISOString().split('T')[0], + startTime: appointmentDate.toTimeString().split(' ')[0].substring(0, 5), + endTime: new Date(appointmentDate.getTime() + appointment.duration * 60000).toTimeString().split(' ')[0].substring(0, 5), + status: appointment.status, + appointmentType: appointment.type, + notes: appointment.notes, + cpf: '', + rg: '', + birthDate: '', + phoneCode: '+55', + phoneNumber: '', + email: '', + unit: 'nei', + }; + }; + + const handleDelete = (appointmentId: string) => { + if (window.confirm("Tem certeza que deseja excluir esta consulta?")) { + setAppointments((prev) => prev.filter((a) => a.id !== appointmentId)); + } + }; + + const handleEdit = (appointment: any) => { + const formData = mapAppointmentToFormData(appointment); + setEditingAppointment(formData); + setShowForm(true); + }; + + const handleView = (appointment: any) => { + setViewingAppointment(appointment); + }; + + const handleCancel = () => { + setEditingAppointment(null); + setShowForm(false); + }; + + const handleSave = (formData: any) => { + + const updatedAppointment = { + id: formData.id, + patient: formData.patientName, + time: new Date(`${formData.appointmentDate}T${formData.startTime}`).toISOString(), + duration: 30, + type: formData.appointmentType as any, + status: formData.status as any, + professional: appointments.find(a => a.id === formData.id)?.professional || '', + notes: formData.notes, + }; + + setAppointments(prev => + prev.map(a => a.id === updatedAppointment.id ? updatedAppointment : a) + ); + handleCancel(); + }; + + if (showForm && editingAppointment) { + return ( +
+
+ +

Editar Consulta

+
+ +
+ ) + } + + return ( +
+
+
+

Gerenciamento de Consultas

+

Visualize, filtre e gerencie todas as consultas da clínica.

+
+
+ + + +
+
+ + + + Consultas Agendadas + + Visualize, filtre e gerencie todas as consultas da clínica. + +
+
+ + +
+ + +
+
+ + + + + Paciente + Médico + Status + Data e Hora + Ações + + + + {appointments.map((appointment) => { + const professional = mockProfessionals.find( + (p) => p.id === appointment.professional + ); + return ( + + + {appointment.patient} + + + {professional ? professional.name : "Não encontrado"} + + + + {capitalize(appointment.status)} + + + {formatDate(appointment.time)} + + + + + + + handleView(appointment)} + > + + Ver + + handleEdit(appointment)}> + + Editar + + handleDelete(appointment.id)} + className="text-destructive" + > + + Excluir + + + + + + ); + })} + +
+
+
+ + {viewingAppointment && ( + setViewingAppointment(null)}> + + + Detalhes da Consulta + + Informações detalhadas da consulta de {viewingAppointment?.patient}. + + +
+
+ + {viewingAppointment?.patient} +
+
+ + + {mockProfessionals.find(p => p.id === viewingAppointment?.professional)?.name || "Não encontrado"} + +
+
+ + {viewingAppointment?.time ? formatDate(viewingAppointment.time) : ''} +
+
+ + + + {capitalize(viewingAppointment?.status || '')} + + +
+
+ + {capitalize(viewingAppointment?.type || '')} +
+
+ + {viewingAppointment?.notes || "Nenhuma"} +
+
+ + + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/susconecta/app/(main-routes)/dashboard/page.tsx b/susconecta/app/(main-routes)/dashboard/page.tsx new file mode 100644 index 0000000..d4f8449 --- /dev/null +++ b/susconecta/app/(main-routes)/dashboard/page.tsx @@ -0,0 +1,41 @@ +export default function DashboardPage() { + return ( + <> +
+
+

Dashboard

+

+ Bem-vindo ao painel de controle +

+
+ +
+
+

+ Total de Pacientes +

+

1,234

+
+
+

+ Consultas Hoje +

+

28

+
+
+

+ Próximas Consultas +

+

45

+
+
+

+ Receita Mensal +

+

R$ 45.230

+
+
+
+ + ); +} diff --git a/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx new file mode 100644 index 0000000..8147c76 --- /dev/null +++ b/susconecta/app/(main-routes)/dashboard/relatorios/page.tsx @@ -0,0 +1,88 @@ +"use client"; + + +import { Button } from "@/components/ui/button"; +import { FileDown } from "lucide-react"; +import jsPDF from "jspdf"; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; + +export default function RelatoriosPage() { + // Dados fictícios para o gráfico financeiro + const financeiro = [ + { mes: "Jan", faturamento: 35000, despesas: 12000 }, + { mes: "Fev", faturamento: 29000, despesas: 15000 }, + { mes: "Mar", faturamento: 42000, despesas: 18000 }, + { mes: "Abr", faturamento: 38000, despesas: 14000 }, + { mes: "Mai", faturamento: 45000, despesas: 20000 }, + { mes: "Jun", faturamento: 41000, despesas: 17000 }, + ]; + // ============================ + // PASSO 3 - Funções de exportar + // ============================ + const exportConsultasPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório de Consultas", 10, 10); + doc.text("Resumo das consultas realizadas.", 10, 20); + doc.save("relatorio-consultas.pdf"); + }; + + const exportPacientesPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório de Pacientes", 10, 10); + doc.text("Informações gerais dos pacientes cadastrados.", 10, 20); + doc.save("relatorio-pacientes.pdf"); + }; + + const exportFinanceiroPDF = () => { + const doc = new jsPDF(); + doc.text("Relatório Financeiro", 10, 10); + doc.text("Receitas e despesas da clínica.", 10, 20); + doc.save("relatorio-financeiro.pdf"); + }; + + return ( +
+

Relatórios

+ +
+ {/* Card Consultas */} +
+

Relatório de Consultas

+

Resumo das consultas realizadas.

+ {/* PASSO 4 - Botão chama a função */} + +
+ + {/* Card Pacientes */} +
+

Relatório de Pacientes

+

Informações gerais dos pacientes cadastrados.

+ +
+ + {/* Card Financeiro com gráfico */} +
+

Relatório Financeiro

+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/doutores/page.tsx b/susconecta/app/(main-routes)/doutores/page.tsx new file mode 100644 index 0000000..588e7f9 --- /dev/null +++ b/susconecta/app/(main-routes)/doutores/page.tsx @@ -0,0 +1,238 @@ +"use client"; + +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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form"; + + +import { listarMedicos, excluirMedico, Medico } from "@/lib/api"; + +export default function DoutoresPage() { + const [doctors, setDoctors] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + const [viewingDoctor, setViewingDoctor] = useState(null); + + + async function load() { + setLoading(true); + try { + const list = await listarMedicos({ limit: 50 }); + setDoctors(list ?? []); + } finally { + setLoading(false); + } + } + + useEffect(() => { + load(); + }, []); + + const filtered = useMemo(() => { + if (!search.trim()) return doctors; + const q = search.toLowerCase(); + return doctors.filter((d) => { + const byName = (d.nome || "").toLowerCase().includes(q); + const byCrm = (d.crm || "").toLowerCase().includes(q); + const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q); + return byName || byCrm || byEspecialidade; + }); + }, [doctors, search]); + + function handleAdd() { + setEditingId(null); + setShowForm(true); + } + + function handleEdit(id: string) { + setEditingId(id); + setShowForm(true); + } + + function handleView(doctor: Medico) { + setViewingDoctor(doctor); + } + + + async function handleDelete(id: string) { + if (!confirm("Excluir este médico?")) return; + await excluirMedico(id); + await load(); + } + + + async function handleSaved() { + setShowForm(false); + await load(); + } + + if (showForm) { + return ( +
+
+ +

{editingId ? "Editar Médico" : "Novo Médico"}

+
+ + setShowForm(false)} + /> +
+ ); + } + + return ( +
+
+
+

Médicos

+

Gerencie os médicos da sua clínica

+
+ +
+
+ + setSearch(e.target.value)} + /> +
+ +
+
+ +
+ + + + Nome + Especialidade + CRM + Contato + Ações + + + + {loading ? ( + + + Carregando… + + + ) : filtered.length > 0 ? ( + filtered.map((doctor) => ( + + {doctor.nome} + + {doctor.especialidade} + + {doctor.crm} + +
+ {doctor.email} + {doctor.telefone} +
+
+ + + + + + + handleView(doctor)}> + + Ver + + handleEdit(String(doctor.id))}> + + Editar + + handleDelete(String(doctor.id))} className="text-destructive"> + + Excluir + + + + +
+ )) + ) : ( + + + Nenhum médico encontrado + + + )} +
+
+
+ + {viewingDoctor && ( + setViewingDoctor(null)}> + + + Detalhes do Médico + + Informações detalhadas de {viewingDoctor?.nome}. + + +
+
+ + {viewingDoctor?.nome} +
+
+ + + {viewingDoctor?.especialidade} + +
+
+ + {viewingDoctor?.crm} +
+
+ + {viewingDoctor?.email} +
+
+ + {viewingDoctor?.telefone} +
+
+ + + +
+
+ )} + +
+ Mostrando {filtered.length} de {doctors.length} +
+
+ ); +} diff --git a/susconecta/app/(main-routes)/layout.tsx b/susconecta/app/(main-routes)/layout.tsx new file mode 100644 index 0000000..d0e759f --- /dev/null +++ b/susconecta/app/(main-routes)/layout.tsx @@ -0,0 +1,25 @@ +import type React from "react"; +import ProtectedRoute from "@/components/ProtectedRoute"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { PagesHeader } from "@/components/dashboard/header"; + +export default function MainRoutesLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + +
+ + +
+ + {children} +
+
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/pacientes/loading.tsx b/susconecta/app/(main-routes)/pacientes/loading.tsx new file mode 100644 index 0000000..f15322a --- /dev/null +++ b/susconecta/app/(main-routes)/pacientes/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/susconecta/app/(main-routes)/pacientes/page.tsx b/susconecta/app/(main-routes)/pacientes/page.tsx new file mode 100644 index 0000000..d4a25ff --- /dev/null +++ b/susconecta/app/(main-routes)/pacientes/page.tsx @@ -0,0 +1,297 @@ + +"use client"; + +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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +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 [patients, setPatients] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [search, setSearch] = useState(""); + const [showForm, setShowForm] = useState(false); + const [editingId, setEditingId] = useState(null); + const [viewingPatient, setViewingPatient] = useState(null); + + 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); + } + } + + useEffect(() => { + loadAll(); + }, []); + + 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); + } + + function handleView(patient: Paciente) { + setViewingPatient(patient); + } + + 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

Carregando pacientes...

; + if (error) return

{error}

; + + if (showForm) { + return ( +
+
+ +

{editingId ? "Editar paciente" : "Novo paciente"}

+
+ + setShowForm(false)} + /> +
+ ); + } + + return ( +
+
+
+

Pacientes

+

Gerencie os pacientes

+
+ +
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()} + /> +
+ + +
+
+ +
+ + + + Nome + CPF + Telefone + Cidade + Estado + Ações + + + + {filtered.length > 0 ? ( + filtered.map((p) => ( + + {p.nome || "(sem nome)"} + {p.cpf || "-"} + {p.telefone || "-"} + {p.endereco?.cidade || "-"} + {p.endereco?.estado || "-"} + + + + + + + handleView(p)}> + + Ver + + handleEdit(String(p.id))}> + + Editar + + handleDelete(String(p.id))} className="text-destructive"> + + Excluir + + + + + + )) + ) : ( + + + Nenhum paciente encontrado + + + )} + +
+
+ + {viewingPatient && ( + setViewingPatient(null)}> + + + Detalhes do Paciente + + Informações detalhadas de {viewingPatient.nome}. + + +
+
+ + {viewingPatient.nome} +
+
+ + {viewingPatient.cpf} +
+
+ + {viewingPatient.telefone} +
+
+ + + {`${viewingPatient.endereco?.logradouro || ''}, ${viewingPatient.endereco?.numero || ''} - ${viewingPatient.endereco?.bairro || ''}, ${viewingPatient.endereco?.cidade || ''} - ${viewingPatient.endereco?.estado || ''}`} + +
+
+ + {viewingPatient.observacoes || "Nenhuma"} +
+
+ + + +
+
+ )} + +
Mostrando {filtered.length} de {patients.length}
+
+ ); +} diff --git a/susconecta/app/agenda/page.tsx b/susconecta/app/agenda/page.tsx new file mode 100644 index 0000000..e639578 --- /dev/null +++ b/susconecta/app/agenda/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; +import HeaderAgenda from "@/components/agenda/HeaderAgenda"; +import FooterAgenda from "@/components/agenda/FooterAgenda"; + +export default function NovoAgendamentoPage() { + const router = useRouter(); + + const handleSave = (data: any) => { + console.log("Salvando novo agendamento...", data); + alert("Novo agendamento salvo (simulado)!"); + router.push("/consultas"); + }; + + const handleCancel = () => { + router.back(); + }; + + return ( +
+ +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/susconecta/app/globals.css b/susconecta/app/globals.css new file mode 100644 index 0000000..817775e --- /dev/null +++ b/susconecta/app/globals.css @@ -0,0 +1,90 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: #ffffff; + --foreground: #475569; + --card: #ffffff; + --card-foreground: #334155; + --popover: #ffffff; + --popover-foreground: #475569; + --primary: var(--color-blue-500); + --primary-foreground: #ffffff; + --secondary: #e2e8f0; + --secondary-foreground: #475569; + --muted: #f1f5f9; + --muted-foreground: #64748b; + --accent: #0891b2; + --accent-foreground: #ffffff; + --destructive: #dc2626; + --destructive-foreground: #ffffff; + --border: #e2e8f0; + --input: #f1f5f9; + --ring: var(--color-blue-500); + --chart-1: #0891b2; + --chart-2: #0f766e; + --chart-3: #f59e0b; + --chart-4: #dc2626; + --chart-5: #475569; + --radius: 0.5rem; + --sidebar: #ffffff; + --sidebar-foreground: #475569; + --sidebar-primary: var(--color-blue-500); + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: var(--color-blue-500); + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #e2e8f0; + --sidebar-ring: var(--color-blue-500); +} + +@theme inline { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground font-sans; + } +} diff --git a/susconecta/app/layout.tsx b/susconecta/app/layout.tsx new file mode 100644 index 0000000..52ed9d1 --- /dev/null +++ b/susconecta/app/layout.tsx @@ -0,0 +1,28 @@ +import type React from "react" +import type { Metadata } from "next" +import { AuthProvider } from "@/hooks/useAuth" +import "./globals.css" + +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' +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + {children} + + + + ) +} diff --git a/susconecta/app/login-admin/page.tsx b/susconecta/app/login-admin/page.tsx new file mode 100644 index 0000000..8570b02 --- /dev/null +++ b/susconecta/app/login-admin/page.tsx @@ -0,0 +1,123 @@ +'use client' +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { useAuth } from '@/hooks/useAuth' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' + +export default function LoginAdminPage() { + const [credentials, setCredentials] = useState({ email: '', password: '' }) + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const router = useRouter() + const { login } = useAuth() + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + // Simular delay de autenticação + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Tentar fazer login usando o contexto com tipo administrador + const success = login(credentials.email, credentials.password, 'administrador') + + if (success) { + // Redirecionar para o dashboard do administrador + setTimeout(() => { + router.push('/dashboard') + + // Fallback: usar window.location se router.push não funcionar + setTimeout(() => { + if (window.location.pathname === '/login-admin') { + window.location.href = '/dashboard' + } + }, 100) + }, 100) + } else { + setError('Email ou senha incorretos') + } + + setLoading(false) + } + + return ( +
+
+
+

+ Login Administrador de Clínica +

+

+ Entre com suas credenciais para acessar o sistema administrativo +

+
+ + + + Acesso Administrativo + + +
+
+ + setCredentials({...credentials, email: e.target.value})} + required + className="mt-1" + /> +
+ +
+ + setCredentials({...credentials, password: e.target.value})} + required + className="mt-1" + /> +
+ + {error && ( + + {error} + + )} + + +
+ +
+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/susconecta/app/login/page.tsx b/susconecta/app/login/page.tsx new file mode 100644 index 0000000..b308b85 --- /dev/null +++ b/susconecta/app/login/page.tsx @@ -0,0 +1,123 @@ +'use client' +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { useAuth } from '@/hooks/useAuth' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' + +export default function LoginPage() { + const [credentials, setCredentials] = useState({ email: '', password: '' }) + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + const router = useRouter() + const { login } = useAuth() + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + // Simular delay de autenticação + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Tentar fazer login usando o contexto com tipo profissional + const success = login(credentials.email, credentials.password, 'profissional') + + if (success) { + // Redirecionar para a página do profissional + setTimeout(() => { + router.push('/profissional') + + // Fallback: usar window.location se router.push não funcionar + setTimeout(() => { + if (window.location.pathname === '/login') { + window.location.href = '/profissional' + } + }, 100) + }, 100) + } else { + setError('Email ou senha incorretos') + } + + setLoading(false) + } + + return ( +
+
+
+

+ Login Profissional de Saúde +

+

+ Entre com suas credenciais para acessar o sistema +

+
+ + + + Acesso ao Sistema + + +
+
+ + setCredentials({...credentials, email: e.target.value})} + required + className="mt-1" + /> +
+ +
+ + setCredentials({...credentials, password: e.target.value})} + required + className="mt-1" + /> +
+ + {error && ( + + {error} + + )} + + +
+ +
+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/susconecta/app/page.tsx b/susconecta/app/page.tsx new file mode 100644 index 0000000..3ee76cc --- /dev/null +++ b/susconecta/app/page.tsx @@ -0,0 +1,15 @@ +import { Header } from "@/components/header" +import { HeroSection } from "@/components/hero-section" +import { Footer } from "@/components/footer" + +export default function HomePage() { + return ( +
+
+
+ +
+
+
+ ) +} diff --git a/susconecta/app/procedimento/page.tsx b/susconecta/app/procedimento/page.tsx new file mode 100644 index 0000000..535a02b --- /dev/null +++ b/susconecta/app/procedimento/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Search, ChevronDown, RotateCcw } from "lucide-react"; +import { Plus } from "lucide-react"; +import HeaderAgenda from "@/components/agenda/HeaderAgenda"; +import FooterAgenda from "@/components/agenda/FooterAgenda"; + +export default function ProcedimentoPage() { + const pathname = usePathname(); + const router = useRouter(); + const [bloqueio, setBloqueio] = useState(false); + + const isAg = pathname?.startsWith("/agendamento"); + const isPr = pathname?.startsWith("/procedimento"); + const isFi = pathname?.startsWith("/financeiro"); + const tab = (active: boolean, extra = "") => + `px-4 py-1.5 text-[13px] border ${ + active + ? "border-sky-500 bg-sky-50 text-sky-700 font-medium" + : "text-gray-700 hover:bg-gray-100" + } ${extra}`; + + return ( +
+ {/* HEADER */} + + + {/* CORPO */} +
+ {/* ATENDIMENTOS */} +
+ {/* Selo Atendimento com + dentro da bolinha */} +
+ + + + Atendimento +
+ + {/* Traço separador */} +
+ + {/* PROCEDIMENTOS */} +
+ +
+ + + +
+
+ + {/* Traço separador */} +
+ + {/* OUTRAS DESPESAS */} +
+ +
+ + + +
+
+
+
+ + {/* RODAPÉ FIXO */} + +
+ ); +} diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx new file mode 100644 index 0000000..1625bce --- /dev/null +++ b/susconecta/app/profissional/page.tsx @@ -0,0 +1,1215 @@ +"use client"; + +import React, { useState, useRef } from "react"; +import SignatureCanvas from "react-signature-canvas"; +import ReactQuill from "react-quill"; +import "react-quill/dist/quill.snow.css"; +import Link from "next/link"; +import ProtectedRoute from "@/components/ProtectedRoute"; +import { useAuth } from "@/hooks/useAuth"; +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 { logout, userEmail } = useAuth(); + const [activeSection, setActiveSection] = useState('calendario'); + const [pacienteSelecionado, setPacienteSelecionado] = useState(null); + + // Estados para o perfil do médico + const [isEditingProfile, setIsEditingProfile] = useState(false); + const [profileData, setProfileData] = useState({ + nome: "Dr. Carlos Andrade", + email: userEmail || "carlos.andrade@hospital.com", + telefone: "(11) 99999-9999", + endereco: "Rua das Flores, 123 - Centro", + cidade: "São Paulo", + cep: "01234-567", + crm: "CRM 000000", + especialidade: "Cardiologia", + biografia: "Médico cardiologista com mais de 15 anos de experiência em cirurgias cardíacas e tratamentos preventivos." + }); + + const [events, setEvents] = useState([ + + { + 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(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(null); + const [selectedEvent, setSelectedEvent] = useState(null); + const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date()); + + const handleSave = (event: React.MouseEvent) => { + 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"; + }; + + // Funções para o perfil + const handleProfileChange = (field: string, value: string) => { + setProfileData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSaveProfile = () => { + setIsEditingProfile(false); + alert('Perfil atualizado com sucesso!'); + }; + + const handleCancelEdit = () => { + setIsEditingProfile(false); + }; + + + 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 ( +
+ {eventInfo.event.title} + + {eventInfo.event.extendedProps.type} + + {eventInfo.event.extendedProps.time} +
+ ); + }; + + + const renderCalendarioSection = () => { + const todayEvents = getTodayEvents(); + + return ( +
+
+

Agenda do Dia

+
+ + {/* Navegação de Data */} +
+
+ +

+ {formatDate(currentCalendarDate)} +

+ + +
+
+ {todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''} +
+
+ + {/* Lista de Pacientes do Dia */} +
+ {todayEvents.length === 0 ? ( +
+ +

Nenhuma consulta agendada para este dia

+

Agenda livre para este dia

+
+ ) : ( + todayEvents.map((appointment) => { + const paciente = pacientes.find(p => p.nome === appointment.title); + return ( +
+
+
+
+
+
+ + {appointment.title} +
+ {paciente && ( +
+ CPF: {paciente.cpf} • {paciente.idade} anos +
+ )} +
+
+
+ + {appointment.time} +
+
+
+ {appointment.type} +
+
+
+
+ +
+ Ver informações do paciente +
+
+
+
+
+
+ ); + }) + )} +
+
+ ); + }; + + + const renderPacientesSection = () => ( +
+

Gerenciamento de Pacientes

+ + + + Paciente + CPF + Idade + Status do laudo + Ações + + + + {pacientes.map((paciente) => ( + + {paciente.nome} + {paciente.cpf} + {paciente.idade} + {paciente.statusLaudo} + +
+
+ +
+ Ver informações do paciente +
+
+
+
+
+
+ ))} +
+
+
+ ); + + + const renderProntuarioSection = () => ( +
+

Prontuário do Paciente

+ {pacienteSelecionado && ( +
+
+

Dados do Paciente

+ +
+
+
+ Nome: +

{pacienteSelecionado.nome}

+
+
+ CPF: +

{pacienteSelecionado.cpf}

+
+
+ Idade: +

{pacienteSelecionado.idade} anos

+
+
+
+ )} +
+
+ +

+ 03/09/2025 +

+
+
+
+ + +
+
+ + +
+
+
+ +