Merge pull request 'feature/scheduling' (#5) from feature/scheduling into develop
Reviewed-on: #5
This commit is contained in:
commit
a1ba4e5ee3
1
.gitignore
vendored
1
.gitignore
vendored
@ -0,0 +1 @@
|
||||
node_modules
|
||||
576
package-lock.json
generated
Normal file
576
package-lock.json
generated
Normal file
@ -0,0 +1,576 @@
|
||||
{
|
||||
"name": "riseup-squad20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz",
|
||||
"integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.20.2",
|
||||
"@react-aria/interactions": "^3.25.0",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">= 16 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
|
||||
"integrity": "sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/interactions": "^3.25.5",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.5.tgz",
|
||||
"integrity": "sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.30.1",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.1.tgz",
|
||||
"integrity": "sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.10.8",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/flags": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
|
||||
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz",
|
||||
"integrity": "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
|
||||
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-arithmetic": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz",
|
||||
"integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/globalize": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz",
|
||||
"integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA=="
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar": {
|
||||
"version": "1.19.4",
|
||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.19.4.tgz",
|
||||
"integrity": "sha512-FrvbDx2LF6JAWFD96LU1jjloppC5OgIvMYUYIPzAw5Aq+ArYFPxAjLqXc4DyxfsQDN0TJTMuS/BIbcSB7Pg0YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"clsx": "^1.2.1",
|
||||
"date-arithmetic": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dom-helpers": "^5.2.1",
|
||||
"globalize": "^0.1.1",
|
||||
"invariant": "^2.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.2.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.40",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-overlays": "^5.2.1",
|
||||
"uncontrollable": "^7.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17 || ^18 || ^19",
|
||||
"react-dom": "^16.14.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-overlays": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
|
||||
"integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.8",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@restart/hooks": "^0.4.7",
|
||||
"@types/warning": "^3.0.0",
|
||||
"dom-helpers": "^5.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/uncontrollable": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/react": ">=16.9.11",
|
||||
"invariant": "^2.2.4",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4"
|
||||
}
|
||||
}
|
||||
139
susconecta/app/agendamento/page.tsx
Normal file
139
susconecta/app/agendamento/page.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
119
susconecta/components/agenda/page.tsx
Normal file
119
susconecta/components/agenda/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
303
susconecta/components/agendamento/AgendaCalendar.tsx
Normal file
303
susconecta/components/agendamento/AgendaCalendar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
227
susconecta/components/agendamento/AppointmentModal.tsx
Normal file
227
susconecta/components/agendamento/AppointmentModal.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface Appointment {
|
||||
id?: string;
|
||||
patient: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
type: 'consulta' | 'exame' | 'retorno';
|
||||
status: 'confirmed' | 'pending' | 'absent';
|
||||
professional: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface Professional {
|
||||
id: string;
|
||||
name: string;
|
||||
specialty: string;
|
||||
}
|
||||
|
||||
interface AppointmentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (appointment: Appointment) => void;
|
||||
professionals: Professional[];
|
||||
appointment?: Appointment | null;
|
||||
}
|
||||
|
||||
export default function AppointmentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
professionals,
|
||||
appointment
|
||||
}: AppointmentModalProps) {
|
||||
const [formData, setFormData] = useState<Appointment>({
|
||||
patient: '',
|
||||
time: '',
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (appointment) {
|
||||
setFormData(appointment);
|
||||
} else {
|
||||
setFormData({
|
||||
patient: '',
|
||||
time: '',
|
||||
duration: 30,
|
||||
type: 'consulta',
|
||||
status: 'pending',
|
||||
professional: professionals[0]?.id || '',
|
||||
notes: ''
|
||||
});
|
||||
}
|
||||
}, [appointment, professionals]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{appointment ? 'Editar Agendamento' : 'Novo Agendamento'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Paciente
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="patient"
|
||||
value={formData.patient}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Profissional
|
||||
</label>
|
||||
<select
|
||||
name="professional"
|
||||
value={formData.professional}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="">Selecione um profissional</option>
|
||||
{professionals.map(prof => (
|
||||
<option key={prof.id} value={prof.id}>
|
||||
{prof.name} - {prof.specialty}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data e Hora
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="time"
|
||||
value={formData.time}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Duração (min)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="duration"
|
||||
value={formData.duration}
|
||||
onChange={handleChange}
|
||||
min="15"
|
||||
step="15"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo
|
||||
</label>
|
||||
<select
|
||||
name="type"
|
||||
value={formData.type}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="consulta">Consulta</option>
|
||||
<option value="exame">Exame</option>
|
||||
<option value="retorno">Retorno</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="pending">Pendente</option>
|
||||
<option value="confirmed">Confirmado</option>
|
||||
<option value="absent">Ausente</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observações
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes || ''}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Salvar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
144
susconecta/components/agendamento/ListaEspera.tsx
Normal file
144
susconecta/components/agendamento/ListaEspera.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
4
susconecta/components/agendamento/index.ts
Normal file
4
susconecta/components/agendamento/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// components/agendamento/index.ts
|
||||
export { default as AgendaCalendar } from './AgendaCalendar';
|
||||
export { default as AppointmentModal } from './AppointmentModal';
|
||||
export { default as ListaEspera } from './ListaEspera';
|
||||
@ -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 },
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -698,6 +698,7 @@ function SidebarMenuSubButton({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
||||
95
susconecta/hooks/UseAgenda.ts
Normal file
95
susconecta/hooks/UseAgenda.ts
Normal file
@ -0,0 +1,95 @@
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user