develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
13 changed files with 1646 additions and 20 deletions
Showing only changes of commit a1ba4e5ee3 - Show all commits

1
.gitignore vendored
View File

@ -0,0 +1 @@
node_modules

576
package-lock.json generated Normal file
View 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
View 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"
}
}

View File

@ -0,0 +1,139 @@
// app/agendamento/page.tsx
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
// Importação dinâmica para evitar erros de SSR
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 });
// Dados mockados
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>
);
}

View File

@ -0,0 +1,119 @@
// app/agenda/page.tsx
'use client';
import { useState } from 'react';
import { AgendaCalendar, AppointmentModal, ListaEspera } from '@/components/agendamento';
// Dados mockados - substitua pelos seus dados reais
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) {
// Editar agendamento existente
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
} else {
// Novo agendamento
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) => {
// Lógica para notificar paciente
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>
);
}

View File

@ -0,0 +1,303 @@
// components/agendamento/AgendaCalendar.tsx (atualizado)
'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());
};
// Filtra os agendamentos por profissional selecionado
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>
{/* Visualização de Dia/Semana (calendário) */}
{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>
)}
{/* Visualização de Mês (lista) */}
{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>
);
}

View 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>
);
}

View File

@ -0,0 +1,144 @@
// components/agendamento/ListaEspera.tsx
'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>
);
}

View 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';

View File

@ -7,7 +7,7 @@ import { Home, Calendar, Users, UserCheck, FileText, BarChart3, Settings, Stetho
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: "Consultas", href: "/dashboard/consultas", icon: UserCheck },
{ name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText },

View File

@ -40,15 +40,19 @@ export function Header() {
>
Sou Paciente
</Button>
<Button asChild className="bg-primary hover:bg-primary/90 text-primary-foreground">
<Link href="/profissional">Sou Profissional de Saúde</Link>
<Link href="/dashboard">
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
Sou Profissional de Saúde
</Button>
</Link>
<Link href="/dashboard">
<Button
variant="outline"
className="text-slate-700 border-slate-600 hover:bg-slate-700 hover:text-white bg-transparent"
>
Sou Administrador de uma Clínica
</Button>
</Link>
</div>
{/* Mobile Menu Button */}
@ -78,19 +82,23 @@ export function Header() {
<div className="flex flex-col space-y-2 pt-4">
<Button
variant="outline"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent w-full"
>
Sou Paciente
</Button>
<Button asChild className="bg-primary hover:bg-primary/90 text-primary-foreground">
<Link href="/profissional" onClick={() => setIsMenuOpen(false)}>Sou Profissional de Saúde</Link>
<Link href="/dashboard" onClick={() => setIsMenuOpen(false)}>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
Sou Profissional de Saúde
</Button>
</Link>
<Link href="/dashboard" onClick={() => setIsMenuOpen(false)}>
<Button
variant="outline"
className="text-slate-700 border-slate-600 hover:bg-slate-700 hover:text-white bg-transparent"
className="text-slate-700 border-slate-600 hover:bg-slate-700 hover:text-white bg-transparent w-full"
>
Sou Administrador de uma Clínica
</Button>
</Link>
</div>
</nav>
</div>
@ -99,3 +107,4 @@ export function Header() {
</header>
)
}

View File

@ -698,6 +698,7 @@ function SidebarMenuSubButton({
)
}
export {
Sidebar,
SidebarContent,

View File

@ -0,0 +1,95 @@
// hooks/useAgenda.ts
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) {
// Editar agendamento existente
setAppointments(prev => prev.map(a => a.id === appointment.id ? appointment : a));
} else {
// Novo agendamento
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) => {
// Lógica para notificar paciente
console.log(`Notificando paciente ${patientId}`);
};
const handleAddToWaitlist = () => {
setIsWaitlistModalOpen(true);
};
return {
appointments,
waitingList,
professionals,
isModalOpen,
selectedAppointment,
isWaitlistModalOpen,
handleSaveAppointment,
handleEditAppointment,
handleAddAppointment,
handleCloseModal,
handleNotifyPatient,
handleAddToWaitlist,
};
};