feat: melhorias no formulário de paciente e avatar

This commit is contained in:
guisilvagomes 2025-10-15 15:26:53 -03:00
parent e1e061c461
commit e443cb1135
15 changed files with 932 additions and 491 deletions

View File

@ -30,7 +30,7 @@
"@types/node": "^24.6.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "4.3.2",
"@vitejs/plugin-react": "5.0.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
@ -40,7 +40,7 @@
"tailwindcss": "^3.4.17",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "5.4.10"
"vite": "^7.1.10"
},
"pnpm": {
"overrides": {

View File

@ -63,8 +63,8 @@ importers:
specifier: ^18.3.0
version: 18.3.7(@types/react@18.3.26)
'@vitejs/plugin-react':
specifier: 4.3.2
version: 4.3.2(vite@5.4.10(@types/node@24.7.2))
specifier: 5.0.4
version: 5.0.4(vite@7.1.10(@types/node@24.7.2)(jiti@1.21.7))
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.6)
@ -93,8 +93,8 @@ importers:
specifier: ^8.3.0
version: 8.46.0(eslint@9.37.0(jiti@1.21.7))(typescript@5.9.3)
vite:
specifier: 5.4.10
version: 5.4.10(@types/node@24.7.2)
specifier: ^7.1.10
version: 7.1.10(@types/node@24.7.2)(jiti@1.21.7)
packages:
@ -204,23 +204,17 @@ packages:
resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==}
engines: {node: '>=18.0.0'}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.10':
resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/aix-ppc64@0.25.11':
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.10':
resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
@ -228,10 +222,10 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
cpu: [arm]
'@esbuild/android-arm64@0.25.11':
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.10':
@ -240,10 +234,10 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/android-arm@0.25.11':
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.10':
@ -252,11 +246,11 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/android-x64@0.25.11':
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.10':
resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
@ -264,10 +258,10 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/darwin-arm64@0.25.11':
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.10':
@ -276,11 +270,11 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/darwin-x64@0.25.11':
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.10':
resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
@ -288,10 +282,10 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/freebsd-arm64@0.25.11':
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.10':
@ -300,11 +294,11 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/freebsd-x64@0.25.11':
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.10':
resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
@ -312,10 +306,10 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
cpu: [arm]
'@esbuild/linux-arm64@0.25.11':
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.10':
@ -324,10 +318,10 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
cpu: [ia32]
'@esbuild/linux-arm@0.25.11':
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.10':
@ -336,10 +330,10 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
cpu: [loong64]
'@esbuild/linux-ia32@0.25.11':
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.10':
@ -348,10 +342,10 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
cpu: [mips64el]
'@esbuild/linux-loong64@0.25.11':
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.10':
@ -360,10 +354,10 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
cpu: [ppc64]
'@esbuild/linux-mips64el@0.25.11':
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.10':
@ -372,10 +366,10 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
cpu: [riscv64]
'@esbuild/linux-ppc64@0.25.11':
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.10':
@ -384,10 +378,10 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
cpu: [s390x]
'@esbuild/linux-riscv64@0.25.11':
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.10':
@ -396,10 +390,10 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/linux-s390x@0.25.11':
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.10':
@ -408,16 +402,22 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.11':
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.10':
resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/netbsd-arm64@0.25.11':
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.10':
@ -426,16 +426,22 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.11':
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.10':
resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/openbsd-arm64@0.25.11':
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.10':
@ -444,17 +450,23 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.11':
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.10':
resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/openharmony-arm64@0.25.11':
resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.10':
resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
@ -462,11 +474,11 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/sunos-x64@0.25.11':
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.10':
resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
@ -474,10 +486,10 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
cpu: [ia32]
'@esbuild/win32-arm64@0.25.11':
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.10':
@ -486,10 +498,10 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
cpu: [x64]
'@esbuild/win32-ia32@0.25.11':
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.10':
@ -498,6 +510,12 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.11':
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.0':
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -639,6 +657,9 @@ packages:
resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
engines: {node: '>=14.0.0'}
'@rolldown/pluginutils@1.0.0-beta.38':
resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==}
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
@ -866,11 +887,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@vitejs/plugin-react@4.3.2':
resolution: {integrity: sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==}
engines: {node: ^14.18.0 || >=16.0.0}
'@vitejs/plugin-react@5.0.4':
resolution: {integrity: sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vue/compiler-core@3.5.22':
resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
@ -1338,16 +1359,16 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
hasBin: true
esbuild@0.25.10:
resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.11:
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@ -1464,6 +1485,15 @@ packages:
fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
@ -2157,8 +2187,8 @@ packages:
react: '>=16'
react-dom: '>=16'
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react-router-dom@6.30.1:
@ -2387,6 +2417,10 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
@ -2479,22 +2513,27 @@ packages:
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
vite@5.4.10:
resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
engines: {node: ^18.0.0 || >=20.0.0}
vite@7.1.10:
resolution: {integrity: sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
@ -2509,6 +2548,10 @@ packages:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -2718,153 +2761,162 @@ snapshots:
'@whatwg-node/promise-helpers': 1.3.2
tslib: 2.8.1
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/aix-ppc64@0.25.10':
optional: true
'@esbuild/android-arm64@0.21.5':
'@esbuild/aix-ppc64@0.25.11':
optional: true
'@esbuild/android-arm64@0.25.10':
optional: true
'@esbuild/android-arm@0.21.5':
'@esbuild/android-arm64@0.25.11':
optional: true
'@esbuild/android-arm@0.25.10':
optional: true
'@esbuild/android-x64@0.21.5':
'@esbuild/android-arm@0.25.11':
optional: true
'@esbuild/android-x64@0.25.10':
optional: true
'@esbuild/darwin-arm64@0.21.5':
'@esbuild/android-x64@0.25.11':
optional: true
'@esbuild/darwin-arm64@0.25.10':
optional: true
'@esbuild/darwin-x64@0.21.5':
'@esbuild/darwin-arm64@0.25.11':
optional: true
'@esbuild/darwin-x64@0.25.10':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
'@esbuild/darwin-x64@0.25.11':
optional: true
'@esbuild/freebsd-arm64@0.25.10':
optional: true
'@esbuild/freebsd-x64@0.21.5':
'@esbuild/freebsd-arm64@0.25.11':
optional: true
'@esbuild/freebsd-x64@0.25.10':
optional: true
'@esbuild/linux-arm64@0.21.5':
'@esbuild/freebsd-x64@0.25.11':
optional: true
'@esbuild/linux-arm64@0.25.10':
optional: true
'@esbuild/linux-arm@0.21.5':
'@esbuild/linux-arm64@0.25.11':
optional: true
'@esbuild/linux-arm@0.25.10':
optional: true
'@esbuild/linux-ia32@0.21.5':
'@esbuild/linux-arm@0.25.11':
optional: true
'@esbuild/linux-ia32@0.25.10':
optional: true
'@esbuild/linux-loong64@0.21.5':
'@esbuild/linux-ia32@0.25.11':
optional: true
'@esbuild/linux-loong64@0.25.10':
optional: true
'@esbuild/linux-mips64el@0.21.5':
'@esbuild/linux-loong64@0.25.11':
optional: true
'@esbuild/linux-mips64el@0.25.10':
optional: true
'@esbuild/linux-ppc64@0.21.5':
'@esbuild/linux-mips64el@0.25.11':
optional: true
'@esbuild/linux-ppc64@0.25.10':
optional: true
'@esbuild/linux-riscv64@0.21.5':
'@esbuild/linux-ppc64@0.25.11':
optional: true
'@esbuild/linux-riscv64@0.25.10':
optional: true
'@esbuild/linux-s390x@0.21.5':
'@esbuild/linux-riscv64@0.25.11':
optional: true
'@esbuild/linux-s390x@0.25.10':
optional: true
'@esbuild/linux-x64@0.21.5':
'@esbuild/linux-s390x@0.25.11':
optional: true
'@esbuild/linux-x64@0.25.10':
optional: true
'@esbuild/linux-x64@0.25.11':
optional: true
'@esbuild/netbsd-arm64@0.25.10':
optional: true
'@esbuild/netbsd-x64@0.21.5':
'@esbuild/netbsd-arm64@0.25.11':
optional: true
'@esbuild/netbsd-x64@0.25.10':
optional: true
'@esbuild/netbsd-x64@0.25.11':
optional: true
'@esbuild/openbsd-arm64@0.25.10':
optional: true
'@esbuild/openbsd-x64@0.21.5':
'@esbuild/openbsd-arm64@0.25.11':
optional: true
'@esbuild/openbsd-x64@0.25.10':
optional: true
'@esbuild/openbsd-x64@0.25.11':
optional: true
'@esbuild/openharmony-arm64@0.25.10':
optional: true
'@esbuild/sunos-x64@0.21.5':
'@esbuild/openharmony-arm64@0.25.11':
optional: true
'@esbuild/sunos-x64@0.25.10':
optional: true
'@esbuild/win32-arm64@0.21.5':
'@esbuild/sunos-x64@0.25.11':
optional: true
'@esbuild/win32-arm64@0.25.10':
optional: true
'@esbuild/win32-ia32@0.21.5':
'@esbuild/win32-arm64@0.25.11':
optional: true
'@esbuild/win32-ia32@0.25.10':
optional: true
'@esbuild/win32-x64@0.21.5':
'@esbuild/win32-ia32@0.25.11':
optional: true
'@esbuild/win32-x64@0.25.10':
optional: true
'@esbuild/win32-x64@0.25.11':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@1.21.7))':
dependencies:
eslint: 9.37.0(jiti@1.21.7)
@ -3086,6 +3138,8 @@ snapshots:
'@remix-run/router@1.23.0': {}
'@rolldown/pluginutils@1.0.0-beta.38': {}
'@rollup/pluginutils@5.3.0(rollup@4.52.4)':
dependencies:
'@types/estree': 1.0.8
@ -3326,14 +3380,15 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-react@4.3.2(vite@5.4.10(@types/node@24.7.2))':
'@vitejs/plugin-react@5.0.4(vite@7.1.10(@types/node@24.7.2)(jiti@1.21.7))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4)
'@rolldown/pluginutils': 1.0.0-beta.38
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
vite: 5.4.10(@types/node@24.7.2)
react-refresh: 0.17.0
vite: 7.1.10(@types/node@24.7.2)(jiti@1.21.7)
transitivePeerDependencies:
- supports-color
@ -3795,32 +3850,6 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
'@esbuild/android-arm': 0.21.5
'@esbuild/android-arm64': 0.21.5
'@esbuild/android-x64': 0.21.5
'@esbuild/darwin-arm64': 0.21.5
'@esbuild/darwin-x64': 0.21.5
'@esbuild/freebsd-arm64': 0.21.5
'@esbuild/freebsd-x64': 0.21.5
'@esbuild/linux-arm': 0.21.5
'@esbuild/linux-arm64': 0.21.5
'@esbuild/linux-ia32': 0.21.5
'@esbuild/linux-loong64': 0.21.5
'@esbuild/linux-mips64el': 0.21.5
'@esbuild/linux-ppc64': 0.21.5
'@esbuild/linux-riscv64': 0.21.5
'@esbuild/linux-s390x': 0.21.5
'@esbuild/linux-x64': 0.21.5
'@esbuild/netbsd-x64': 0.21.5
'@esbuild/openbsd-x64': 0.21.5
'@esbuild/sunos-x64': 0.21.5
'@esbuild/win32-arm64': 0.21.5
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
esbuild@0.25.10:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.10
@ -3850,6 +3879,35 @@ snapshots:
'@esbuild/win32-ia32': 0.25.10
'@esbuild/win32-x64': 0.25.10
esbuild@0.25.11:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.11
'@esbuild/android-arm': 0.25.11
'@esbuild/android-arm64': 0.25.11
'@esbuild/android-x64': 0.25.11
'@esbuild/darwin-arm64': 0.25.11
'@esbuild/darwin-x64': 0.25.11
'@esbuild/freebsd-arm64': 0.25.11
'@esbuild/freebsd-x64': 0.25.11
'@esbuild/linux-arm': 0.25.11
'@esbuild/linux-arm64': 0.25.11
'@esbuild/linux-ia32': 0.25.11
'@esbuild/linux-loong64': 0.25.11
'@esbuild/linux-mips64el': 0.25.11
'@esbuild/linux-ppc64': 0.25.11
'@esbuild/linux-riscv64': 0.25.11
'@esbuild/linux-s390x': 0.25.11
'@esbuild/linux-x64': 0.25.11
'@esbuild/netbsd-arm64': 0.25.11
'@esbuild/netbsd-x64': 0.25.11
'@esbuild/openbsd-arm64': 0.25.11
'@esbuild/openbsd-x64': 0.25.11
'@esbuild/openharmony-arm64': 0.25.11
'@esbuild/sunos-x64': 0.25.11
'@esbuild/win32-arm64': 0.25.11
'@esbuild/win32-ia32': 0.25.11
'@esbuild/win32-x64': 0.25.11
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
@ -3999,6 +4057,10 @@ snapshots:
dependencies:
pend: 1.2.0
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fecha@4.2.3: {}
file-entry-cache@8.0.0:
@ -4589,7 +4651,7 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-refresh@0.14.2: {}
react-refresh@0.17.0: {}
react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
@ -4886,6 +4948,11 @@ snapshots:
dependencies:
any-promise: 1.3.0
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tmp-promise@3.0.3:
dependencies:
tmp: 0.2.5
@ -4962,14 +5029,18 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite@5.4.10(@types/node@24.7.2):
vite@7.1.10(@types/node@24.7.2)(jiti@1.21.7):
dependencies:
esbuild: 0.21.5
esbuild: 0.25.11
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.52.4
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.7.2
fsevents: 2.3.3
jiti: 1.21.7
webidl-conversions@3.0.1: {}

View File

@ -1,3 +1,4 @@
import { useState, useEffect, useCallback } from "react";
import {
format,
@ -9,28 +10,25 @@ import {
isSameMonth,
isSameDay,
isToday,
parseISO,
isBefore,
startOfDay,
} from "date-fns";
import { ptBR } from "date-fns/locale";
import {
Search,
Star,
MapPin,
Video,
Clock,
CalendarDays,
ChevronLeft,
ChevronRight,
Stethoscope,
AlertCircle,
CheckCircle2,
Search,
} from "lucide-react";
import { medicoService } from "../services/medicoService";
import { availabilityService } from "../services/availabilityService";
import { exceptionService } from "../services/exceptionService";
import { consultaService } from "../services/consultaService";
import { medicoService } from "../services/medicoService";
interface Medico {
id: string;
@ -81,6 +79,17 @@ const dayOfWeekMap: { [key: number]: keyof Availability } = {
};
export default function AgendamentoConsulta() {
// ...
// ... outras declarações de hooks e funções ...
useEffect(() => {
if (selectedMedico) {
loadDoctorAvailability();
loadDoctorExceptions();
}
}, [selectedMedico, loadDoctorAvailability, loadDoctorExceptions]);
const [medicos, setMedicos] = useState<Medico[]>([]);
const [filteredMedicos, setFilteredMedicos] = useState<Medico[]>([]);
const [selectedMedico, setSelectedMedico] = useState<Medico | null>(null);
@ -104,16 +113,15 @@ export default function AgendamentoConsulta() {
const [bookingError, setBookingError] = useState("");
// Load doctors on mount
useEffect(() => {
loadMedicos();
}, []);
const loadMedicos = async () => {
try {
setLoading(true);
const data = await medicoService.listarMedicos();
setMedicos(data);
setFilteredMedicos(data);
// Supondo que data seja ApiResponse<MedicoListResponse>
if (data && Array.isArray(data.data)) {
setMedicos(data.data);
setFilteredMedicos(data.data);
}
} catch (error) {
console.error("Erro ao carregar médicos:", error);
} finally {
@ -121,6 +129,10 @@ export default function AgendamentoConsulta() {
}
};
useEffect(() => {
loadMedicos();
}, []);
// Filter doctors based on search and specialty
useEffect(() => {
let filtered = medicos;
@ -145,20 +157,24 @@ export default function AgendamentoConsulta() {
// Get unique specialties
const specialties = Array.from(new Set(medicos.map((m) => m.especialidade)));
// Load availability and exceptions when doctor is selected
useEffect(() => {
if (selectedMedico) {
loadDoctorAvailability();
loadDoctorExceptions();
}
}, [selectedMedico]);
const loadDoctorAvailability = async () => {
// ... outras declarações de hooks ...
// ... outras funções e hooks ...
const loadDoctorAvailability = useCallback(async () => {
if (!selectedMedico) return;
try {
const data = await availabilityService.getAvailability(selectedMedico.id);
if (data && data.length > 0) {
const avail = data[0];
const response = await availabilityService.getAvailability(selectedMedico.id);
if (response && response.success && response.data && response.data.length > 0) {
const avail = response.data[0];
setAvailability({
domingo: avail.domingo || { ativo: false, horarios: [] },
segunda: avail.segunda || { ativo: false, horarios: [] },
@ -175,57 +191,52 @@ export default function AgendamentoConsulta() {
console.error("Erro ao carregar disponibilidade:", error);
setAvailability(null);
}
};
}, [selectedMedico]);
const loadDoctorExceptions = async () => {
const loadDoctorExceptions = useCallback(async () => {
if (!selectedMedico) return;
try {
const data = await exceptionService.listExceptions(selectedMedico.id);
setExceptions(data || []);
const response = await exceptionService.listExceptions({ doctor_id: selectedMedico.id });
if (response && response.success && response.data) {
setExceptions(response.data as Exception[]);
} else {
setExceptions([]);
}
} catch (error) {
console.error("Erro ao carregar exceções:", error);
setExceptions([]);
}
};
}, [selectedMedico]);
// Calculate available slots when date is selected
const calculateAvailableSlots = useCallback(() => {
if (!selectedDate || !availability) return;
const dateStr = format(selectedDate, "yyyy-MM-dd");
const isBlocked = exceptions.some((exc) => exc.data === dateStr);
if (isBlocked) {
setAvailableSlots([]);
return;
}
const dayOfWeek = selectedDate.getDay();
const dayKey = dayOfWeekMap[dayOfWeek];
const daySchedule = availability[dayKey];
if (!daySchedule || !daySchedule.ativo) {
setAvailableSlots([]);
return;
}
const slots = daySchedule.horarios
.filter((slot) => slot.ativo)
.map((slot) => slot.inicio);
setAvailableSlots(slots);
}, [selectedDate, availability, exceptions]);
useEffect(() => {
if (selectedDate && availability && selectedMedico) {
calculateAvailableSlots();
} else {
setAvailableSlots([]);
}
}, [selectedDate, availability, exceptions]);
const calculateAvailableSlots = () => {
if (!selectedDate || !availability) return;
// Check if date is an exception (blocked)
const dateStr = format(selectedDate, "yyyy-MM-dd");
const isBlocked = exceptions.some((exc) => exc.data === dateStr);
if (isBlocked) {
setAvailableSlots([]);
return;
}
// Get day of week schedule
const dayOfWeek = selectedDate.getDay();
const dayKey = dayOfWeekMap[dayOfWeek];
const daySchedule = availability[dayKey];
if (!daySchedule || !daySchedule.ativo) {
setAvailableSlots([]);
return;
}
// Extract active time slots
const slots = daySchedule.horarios
.filter((slot) => slot.ativo)
.map((slot) => slot.inicio);
setAvailableSlots(slots);
};
}, [selectedDate, availability, exceptions, calculateAvailableSlots, selectedMedico]);
const isDateBlocked = (date: Date): boolean => {
const dateStr = format(date, "yyyy-MM-dd");
@ -307,20 +318,15 @@ export default function AgendamentoConsulta() {
const user = JSON.parse(userStr);
// Create date-time string
const dataHora = `${format(
selectedDate,
"yyyy-MM-dd"
)}T${selectedTime}:00`;
// Removido: dataHora não é usada
// Book appointment via API
await consultaService.criarConsulta({
medicoId: selectedMedico.id,
pacienteId: user.id,
dataHora,
tipoConsulta: appointmentType,
motivoConsulta: motivo,
status: "agendada",
paciente_id: user.id,
medico_id: selectedMedico.id,
data_hora: format(selectedDate, "yyyy-MM-dd") + "T" + selectedTime,
tipo_consulta: "primeira_vez", // ou "retorno", "emergencia", "rotina" conforme lógica do sistema
motivo_consulta: motivo,
});
setBookingSuccess(true);
@ -334,10 +340,10 @@ export default function AgendamentoConsulta() {
setMotivo("");
setBookingSuccess(false);
}, 3000);
} catch (error: any) {
} catch (error) {
console.error("Erro ao agendar consulta:", error);
setBookingError(
error.message || "Erro ao agendar consulta. Tente novamente."
error instanceof Error ? error.message : "Erro ao agendar consulta. Tente novamente."
);
setShowConfirmDialog(false);
}

View File

@ -5,18 +5,17 @@ import {
Trash2,
Save,
Copy,
Calendar as CalendarIcon,
AlertCircle,
} from "lucide-react";
import toast from "react-hot-toast";
import { format, addDays, startOfWeek } from "date-fns";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import availabilityService from "../services/availabilityService";
import exceptionService from "../services/exceptionService";
import exceptionService, { DoctorException } from "../services/exceptionService";
import { useAuth } from "../hooks/useAuth";
interface TimeSlot {
id: string;
dbId?: string; // ID do banco de dados (se já existir)
inicio: string;
fim: string;
ativo: boolean;
@ -60,44 +59,50 @@ const DisponibilidadeMedico: React.FC = () => {
new Date()
);
const [blockedDates, setBlockedDates] = useState<Date[]>([]);
const [exceptions, setExceptions] = useState<any[]>([]);
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
// Settings
const [consultationDuration, setConsultationDuration] = useState("60");
const [breakTime, setBreakTime] = useState("0");
useEffect(() => {
if (medicoId) {
loadAvailability();
loadExceptions();
}
}, [medicoId]);
const loadAvailability = async () => {
const loadAvailability = React.useCallback(async () => {
try {
setLoading(true);
const response = await availabilityService.getAvailability(medicoId);
// Usar listAvailability ao invés de getAvailability para ter os IDs individuais
const response = await availabilityService.listAvailability({ doctor_id: medicoId });
if (response.success && response.data && response.data.length > 0) {
const availabilityData = response.data[0];
if (response && response.success && response.data && response.data.length > 0) {
const newSchedule: Record<number, DaySchedule> = {};
daysOfWeek.forEach(({ key, label, dbKey }) => {
const dayData = availabilityData[dbKey];
// Inicializar todos os dias
daysOfWeek.forEach(({ key, label }) => {
newSchedule[key] = {
day: label,
dayOfWeek: key,
enabled: dayData?.ativo || false,
slots:
dayData?.horarios?.map((h: any, index: number) => ({
id: `${key}-${index}`,
inicio: h.inicio,
fim: h.fim,
ativo: h.ativo !== false,
})) || [],
enabled: false,
slots: [],
};
});
// Agrupar disponibilidades por dia da semana
response.data.forEach((avail) => {
const weekdayKey = daysOfWeek.find(d => d.dbKey === avail.weekday);
if (!weekdayKey) return;
const dayKey = weekdayKey.key;
if (!newSchedule[dayKey].enabled) {
newSchedule[dayKey].enabled = true;
}
newSchedule[dayKey].slots.push({
id: `${dayKey}-${avail.id || Math.random().toString(36).slice(2)}`,
dbId: avail.id, // Armazenar ID do banco
inicio: avail.start_time?.slice(0, 5) || "09:00",
fim: avail.end_time?.slice(0, 5) || "17:00",
ativo: avail.active ?? true,
});
});
setSchedule(newSchedule);
} else {
// Initialize empty schedule
@ -118,22 +123,29 @@ const DisponibilidadeMedico: React.FC = () => {
} finally {
setLoading(false);
}
};
}, [medicoId]);
const loadExceptions = async () => {
const loadExceptions = React.useCallback(async () => {
try {
const response = await exceptionService.listExceptions(medicoId);
const response = await exceptionService.listExceptions({ doctor_id: medicoId });
if (response.success && response.data) {
setExceptions(response.data);
const blocked = response.data
.filter((exc: any) => exc.tipo === "bloqueio")
.map((exc: any) => new Date(exc.data));
.filter((exc) => exc.kind === "bloqueio" && exc.date)
.map((exc) => new Date(exc.date!));
setBlockedDates(blocked);
}
} catch (error) {
console.error("Erro ao carregar exceções:", error);
}
};
}, [medicoId]);
useEffect(() => {
if (medicoId) {
loadAvailability();
loadExceptions();
}
}, [medicoId, loadAvailability, loadExceptions]);
const toggleDay = (dayKey: number) => {
setSchedule((prev) => ({
@ -169,7 +181,27 @@ const DisponibilidadeMedico: React.FC = () => {
}
};
const removeTimeSlot = (dayKey: number, slotId: string) => {
const removeTimeSlot = async (dayKey: number, slotId: string) => {
const slot = schedule[dayKey]?.slots.find(s => s.id === slotId);
// Se o slot tem um ID do banco, deletar imediatamente
if (slot?.dbId) {
try {
const response = await availabilityService.deleteAvailability(slot.dbId);
if (response.success) {
toast.success("Horário removido com sucesso");
} else {
toast.error(response.error || "Erro ao remover horário");
return;
}
} catch (error) {
console.error("Erro ao remover horário:", error);
toast.error("Erro ao remover horário");
return;
}
}
// Atualizar o estado local
setSchedule((prev) => ({
...prev,
[dayKey]: {
@ -216,36 +248,92 @@ const DisponibilidadeMedico: React.FC = () => {
try {
setSaving(true);
// Build availability object
const availabilityData: any = {
medico_id: medicoId,
if (!medicoId) {
toast.error("Médico não autenticado");
return;
}
const requests: Array<Promise<unknown>> = [];
const timeToMinutes = (t: string) => {
const [hStr, mStr] = t.split(":");
const h = Number(hStr || "0");
const m = Number(mStr || "0");
return h * 60 + m;
};
// Para cada dia, processar slots
daysOfWeek.forEach(({ key, dbKey }) => {
const daySchedule = schedule[key];
availabilityData[dbKey] = {
ativo: daySchedule.enabled,
horarios: daySchedule.slots.map((slot) => ({
inicio: slot.inicio,
fim: slot.fim,
ativo: slot.ativo,
})),
if (!daySchedule || !daySchedule.enabled) {
// Se o dia foi desabilitado, deletar todos os slots existentes
daySchedule?.slots.forEach((slot) => {
if (slot.dbId) {
requests.push(availabilityService.deleteAvailability(slot.dbId));
}
});
return;
}
// Processar cada slot do dia
daySchedule.slots.forEach((slot) => {
const inicio = slot.inicio ? (slot.inicio.length === 5 ? `${slot.inicio}:00` : slot.inicio) : "00:00:00";
const fim = slot.fim ? (slot.fim.length === 5 ? `${slot.fim}:00` : slot.fim) : "00:00:00";
const minutes = Math.max(1, timeToMinutes(fim.slice(0,5)) - timeToMinutes(inicio.slice(0,5)));
const payload = {
weekday: dbKey as "segunda" | "terca" | "quarta" | "quinta" | "sexta" | "sabado" | "domingo",
start_time: inicio,
end_time: fim,
slot_minutes: minutes,
appointment_type: "presencial" as const,
active: !!slot.ativo,
};
if (slot.dbId) {
// Atualizar slot existente
requests.push(availabilityService.updateAvailability(slot.dbId, payload));
} else {
// Criar novo slot
requests.push(availabilityService.createAvailability({
doctor_id: medicoId,
...payload,
}));
}
});
});
const response = await availabilityService.createAvailability(
availabilityData
);
if (response.success) {
toast.success("Disponibilidade salva com sucesso!");
loadAvailability();
} else {
throw new Error(response.message || "Erro ao salvar");
if (requests.length === 0) {
toast.error("Nenhuma alteração para salvar");
return;
}
} catch (error: any) {
const results = await Promise.allSettled(requests);
const errors: string[] = [];
let successCount = 0;
results.forEach((r, idx) => {
if (r.status === "fulfilled") {
const val = r.value as { success?: boolean; error?: string; message?: string };
if (val && val.success) successCount++;
else errors.push(`Item ${idx}: ${val?.error || val?.message || "Erro"}`);
} else {
errors.push(`Item ${idx}: ${r.reason?.message || String(r.reason)}`);
}
});
if (errors.length > 0) {
console.error("Erros ao salvar disponibilidades:", errors);
toast.error(`Algumas disponibilidades não foram salvas (${errors.length})`);
}
if (successCount > 0) {
toast.success(`${successCount} alteração(ões) salvas com sucesso!`);
await loadAvailability();
}
} catch (error) {
console.error("Erro ao salvar disponibilidade:", error);
toast.error(error.message || "Erro ao salvar disponibilidade");
const errorMessage = error instanceof Error ? error.message : "Erro ao salvar disponibilidade";
toast.error(errorMessage);
} finally {
setSaving(false);
}
@ -263,10 +351,10 @@ const DisponibilidadeMedico: React.FC = () => {
if (dateExists) {
// Remove block
const exception = exceptions.find(
(exc) => format(new Date(exc.data), "yyyy-MM-dd") === dateString
(exc) => exc.date && format(new Date(exc.date), "yyyy-MM-dd") === dateString
);
if (exception) {
await exceptionService.deleteException(exception._id);
if (exception && exception.id) {
await exceptionService.deleteException(exception.id);
setBlockedDates(
blockedDates.filter((d) => format(d, "yyyy-MM-dd") !== dateString)
);
@ -275,10 +363,10 @@ const DisponibilidadeMedico: React.FC = () => {
} else {
// Add block
const response = await exceptionService.createException({
medicoId,
data: dateString,
tipo: "bloqueio",
motivo: "Data bloqueada pelo médico",
doctor_id: medicoId,
date: dateString,
kind: "bloqueio",
reason: "Data bloqueada pelo médico",
});
if (response.success) {
setBlockedDates([...blockedDates, selectedDate]);

View File

@ -109,12 +109,7 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
setStatus(editing.status || "agendada");
} else {
setPacienteId(defaultPacienteId || "");
// If user is medico, lock to their id if available
if (user?.role === "medico") {
setMedicoId(user.id);
} else {
setMedicoId(defaultMedicoId || "");
}
setDataHora("");
setTipo("");
setMotivo("");
@ -248,7 +243,7 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
className="w-full border rounded px-2 py-2 text-sm"
value={medicoId}
onChange={(e) => setMedicoId(e.target.value)}
disabled={lockMedico || !!editing || user?.role === "medico"}
disabled={lockMedico || !!editing}
>
<option value="">Selecione...</option>
{medicos.map((m) => (

View File

@ -1,3 +1,5 @@
import { useState, useContext } from "react";
import { AuthContext } from "../../context/AuthContext";
import React from "react";
import type { EnderecoPaciente } from "../../services/pacienteService";
@ -29,6 +31,7 @@ export interface PacienteFormData {
responsavel_cpf?: string;
documentos?: { tipo: string; numero: string }[];
endereco: EnderecoPaciente;
avatar_url?: string;
}
export interface PacienteFormProps {
@ -63,6 +66,57 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
onCancel,
onSubmit,
}) => {
// Avatar upload/remover state
const [avatarEditMode, setAvatarEditMode] = useState(false);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarLoading, setAvatarLoading] = useState(false);
// Obtem role do usuário autenticado
const { user } = useContext(AuthContext);
const canEditAvatar = ["secretaria", "admin", "gestor"].includes(user?.role);
// Função para upload do avatar
const handleAvatarUpload = async () => {
if (!avatarFile || !data.id) return;
setAvatarLoading(true);
const formData = new FormData();
formData.append("file", avatarFile);
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/${data.id}/avatar`, {
method: "POST",
body: formData,
});
// Atualiza avatar_url no perfil
const ext = avatarFile.name.split(".").pop();
const publicUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${data.id}/avatar.${ext}`;
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/profiles?id=eq.${data.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar_url: publicUrl }),
});
onChange({ avatar_url: publicUrl });
setAvatarEditMode(false);
setAvatarFile(null);
setAvatarLoading(false);
};
// Função para remover avatar
const handleAvatarRemove = async () => {
if (!data.id) return;
setAvatarLoading(true);
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/${data.id}/avatar`, {
method: "DELETE",
});
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/profiles?id=eq.${data.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar_url: null }),
});
onChange({ avatar_url: undefined });
setAvatarEditMode(false);
setAvatarFile(null);
setAvatarLoading(false);
};
return (
<form
onSubmit={onSubmit}
@ -70,7 +124,57 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
noValidate
aria-describedby={cpfError ? "cpf-error" : undefined}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Bloco do avatar antes do título dos dados pessoais */}
<div className="flex items-center gap-4 mb-6">
<div className="relative group">
{data.avatar_url ? (
<img
src={data.avatar_url}
alt={data.nome}
className="h-16 w-16 rounded-full object-cover border shadow"
/>
) : (
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-blue-700 to-blue-400 flex items-center justify-center text-white font-semibold text-lg shadow">
{data.nome
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</div>
)}
{canEditAvatar && (
<button
type="button"
className="absolute bottom-0 right-0 bg-white rounded-full p-1 border shadow group-hover:bg-blue-100 transition"
title="Editar avatar"
onClick={() => setAvatarEditMode(true)}
style={{ lineHeight: 0 }}
disabled={avatarLoading}
>
<svg xmlns="http://www.w3.org/2000/svg" className="text-blue-600" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M9 13l6.586-6.586a2 2 0 112.828 2.828L11.828 15.828a2 2 0 01-2.828 0L9 13zm0 0V17h4" /></svg>
</button>
)}
{avatarEditMode && canEditAvatar && (
<div className="absolute top-0 left-20 bg-white p-2 rounded shadow z-10 flex flex-col items-center">
<input
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={e => setAvatarFile(e.target.files?.[0] || null)}
className="mb-2"
disabled={avatarLoading}
/>
<button type="button" className="text-xs bg-blue-600 text-white px-2 py-1 rounded" onClick={handleAvatarUpload} disabled={avatarLoading}>Salvar</button>
<button type="button" className="text-xs ml-2" onClick={() => setAvatarEditMode(false)} disabled={avatarLoading}>Cancelar</button>
{data.avatar_url && (
<button type="button" className="text-xs text-red-600 underline mt-2" onClick={handleAvatarRemove} disabled={avatarLoading}>Remover</button>
)}
</div>
)}
</div>
</div>
{/* Todos os campos do formulário já estão dentro do <form> abaixo do avatar */}
{/* Os campos do formulário devem continuar aqui, dentro do <form> */}
<div className="md:col-span-2">
<h4 className="text-xs font-semibold uppercase tracking-wide text-green-600">
Dados pessoais

View File

@ -14,6 +14,7 @@ export interface PatientListItem {
estado?: string;
ultimoAtendimento?: string | null; // ISO ou texto humanizado
proximoAtendimento?: string | null;
avatar_url?: string;
}
interface PatientListTableProps {
@ -99,12 +100,20 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
{pacientes.map((p) => (
<tr
key={p.id}
className="odd:bg-white even:bg-gray-50/60 dark:even:bg-gray-800/50 hover:bg-blue-50/50 dark:hover:bg-gray-800 transition-colors"
className="bg-white dark:bg-gray-900 hover:bg-blue-50/50 dark:hover:bg-gray-800 transition-colors"
role="row"
>
<td className="px-6 py-4">
<div className="flex items-start gap-3">
{p.avatar_url ? (
<img
src={p.avatar_url}
alt={p.nome}
className="h-10 w-10 rounded-full object-cover border"
/>
) : (
<AvatarInitials name={p.nome} size={40} />
)}
<div>
<div
className="text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer hover:underline"

View File

@ -50,7 +50,7 @@ interface Medico {
}
const AcompanhamentoPaciente: React.FC = () => {
const { user, logout } = useAuth();
const { user, roles = [], logout } = useAuth();
const navigate = useNavigate();
// State
@ -64,8 +64,10 @@ const AcompanhamentoPaciente: React.FC = () => {
const pacienteNome = user?.nome || "Paciente";
useEffect(() => {
if (!user) navigate("/paciente");
}, [user, navigate]);
// Permite acesso se for paciente OU se roles inclui 'paciente'
const isPaciente = user?.role === "paciente" || roles.includes("paciente");
if (!user || !isPaciente) navigate("/paciente");
}, [user, roles, navigate]);
const fetchConsultas = useCallback(async () => {
if (!pacienteId) return;
@ -140,7 +142,9 @@ const AcompanhamentoPaciente: React.FC = () => {
}
try {
await consultaService.atualizarConsulta(consultaId, { status: "cancelada" });
await consultaService.atualizarConsulta(consultaId, {
status: "cancelada",
});
toast.success("Consulta cancelada com sucesso");
fetchConsultas();
} catch (error) {

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
import AvatarInitials from "../components/AvatarInitials";
import { Stethoscope, Mail, Phone, AlertTriangle } from "lucide-react";
import medicoService, { MedicoDetalhado } from "../services/medicoService";
@ -73,6 +74,15 @@ const ListaMedicos: React.FC = () => {
tabIndex={0}
>
<header className="flex items-center gap-2">
{medico.avatar_url ? (
<img
src={medico.avatar_url}
alt={medico.nome}
className="h-10 w-10 rounded-full object-cover border"
/>
) : (
<AvatarInitials name={medico.nome} size={40} />
)}
<Stethoscope className="w-5 h-5 text-indigo-600" />
<h3 className="font-semibold text-lg text-gray-900">
{medico.nome}

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
import AvatarInitials from "../components/AvatarInitials";
// Funções utilitárias para formatação
function formatCPF(cpf?: string) {
if (!cpf) return "Não informado";
@ -85,6 +86,15 @@ const ListaPacientes: React.FC = () => {
tabIndex={0}
>
<div className="flex items-center gap-2 mb-2">
{paciente.avatar_url ? (
<img
src={paciente.avatar_url}
alt={paciente.nome}
className="h-10 w-10 rounded-full object-cover border"
/>
) : (
<AvatarInitials name={paciente.nome} size={40} />
)}
<Users className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-lg">{paciente.nome}</span>
</div>

View File

@ -17,12 +17,13 @@ import {
Plus,
Edit,
Trash2,
Pencil,
} from "lucide-react";
import toast from "react-hot-toast";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useNavigate } from "react-router-dom";
import { Consulta as ServiceConsulta } from "../services/consultasService";
import consultasService, { Consulta as ServiceConsulta } from "../services/consultasService";
import { listPatients } from "../services/pacienteService";
import { useAuth } from "../hooks/useAuth";
import relatorioService, {
@ -30,8 +31,6 @@ import relatorioService, {
} from "../services/relatorioService";
import DisponibilidadeMedico from "../components/DisponibilidadeMedico";
import ConsultaModal from "../components/consultas/ConsultaModal";
import AvailabilityManager from "../components/agenda/AvailabilityManager";
import ExceptionsManager from "../components/agenda/ExceptionsManager";
interface ConsultaUI {
id: string;
@ -45,14 +44,7 @@ interface ConsultaUI {
observacoes?: string;
}
interface Paciente {
_id: string;
nome: string;
telefone: string;
email: string;
convenio: string;
observacoes: string;
}
const PainelMedico: React.FC = () => {
const { user, roles, logout } = useAuth();
@ -66,6 +58,70 @@ const PainelMedico: React.FC = () => {
roles.includes("admin"));
const medicoId = temAcessoMedico ? user.id : "";
const medicoNome = user?.nome || "Médico";
const [avatarUrl, setAvatarUrl] = useState<string | null>(user?.avatar_url || null);
const [avatarEditMode, setAvatarEditMode] = useState(false);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
// Função para buscar avatar público
const fetchAvatarUrl = useCallback(() => {
if (!user?.id) return;
// Tenta jpg, png, webp
const base = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${user.id}/avatar`;
const tryExts = async () => {
for (const ext of ["jpg", "png", "webp"]) {
const url = `${base}.${ext}`;
try {
const res = await fetch(url, { method: "HEAD" });
if (res.ok) {
setAvatarUrl(url);
return;
}
} catch {}
}
setAvatarUrl(null);
};
tryExts();
}, [user?.id]);
useEffect(() => {
fetchAvatarUrl();
}, [fetchAvatarUrl]);
// Upload avatar
const handleAvatarUpload = async () => {
if (!avatarFile || !user?.id) return;
const formData = new FormData();
formData.append("file", avatarFile);
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/${user.id}/avatar`, {
method: "POST",
body: formData,
});
// Atualiza avatar_url no perfil
const ext = avatarFile.name.split(".").pop();
const publicUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${user.id}/avatar.${ext}`;
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/profiles?id=eq.${user.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar_url: publicUrl }),
});
setAvatarEditMode(false);
setAvatarFile(null);
setAvatarUrl(publicUrl);
};
// Remover avatar
const handleAvatarRemove = async () => {
if (!user?.id) return;
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/${user.id}/avatar`, {
method: "DELETE",
});
await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/profiles?id=eq.${user.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar_url: null }),
});
setAvatarUrl(null);
};
// State
const [activeTab, setActiveTab] = useState("dashboard");
@ -99,71 +155,56 @@ const PainelMedico: React.FC = () => {
}, [medicoId, navigate]);
const fetchConsultas = useCallback(async () => {
if (!medicoId) return;
setLoading(true);
try {
const raw = localStorage.getItem("consultas_local");
let lista: ServiceConsulta[] = [];
if (raw) {
let resp;
if (user?.role === "admin" || roles.includes("admin")) {
// Admin: busca todas as consultas do sistema
resp = await consultasService.listarTodas();
} else {
// Médico comum: busca todas as consultas do próprio médico
if (!medicoId) return;
resp = await consultasService.listarPorMedico(medicoId);
}
if (resp && resp.success && resp.data) {
// Buscar nomes dos pacientes usando getPatientById
const { getPatientById } = await import("../services/pacienteService");
const consultasComNomes = await Promise.all(
resp.data.map(async (c) => {
let pacienteNome = "Paciente Desconhecido";
try {
lista = JSON.parse(raw);
} catch {
lista = [];
const pacienteResp = await getPatientById(c.pacienteId);
if (pacienteResp.success && pacienteResp.data) {
pacienteNome = pacienteResp.data.nome;
}
} catch (error) {
console.error("Erro ao buscar nome do paciente:", error);
}
let filtradas = lista.filter((c) => c.medicoId === medicoId);
const hoje = new Date();
if (filtroData === "hoje") {
const dStr = format(hoje, "yyyy-MM-dd");
filtradas = filtradas.filter((c) => c.dataHora.startsWith(dStr));
} else if (filtroData === "amanha") {
const amanha = new Date(hoje);
amanha.setDate(hoje.getDate() + 1);
const dStr = format(amanha, "yyyy-MM-dd");
filtradas = filtradas.filter((c) => c.dataHora.startsWith(dStr));
} else if (filtroData === "semana") {
const start = new Date(hoje);
start.setDate(hoje.getDate() - hoje.getDay());
const end = new Date(start);
end.setDate(start.getDate() + 6);
filtradas = filtradas.filter((c) => {
const d = new Date(c.dataHora);
return d >= start && d <= end;
});
}
const pacientesResponse = await listPatients({ per_page: 200 }).catch(
() => ({ data: [], total: 0, page: 1, per_page: 0 })
);
const pacMap: Record<string, Paciente> = {};
const pacientesLista =
"data" in pacientesResponse ? pacientesResponse.data : [];
pacientesLista.forEach((p) => {
pacMap[p.id] = {
_id: p.id,
nome: p.nome,
telefone: p.telefone || "",
email: p.email || "",
convenio: p.convenio || "",
observacoes: p.observacoes || "",
};
});
setConsultas(
filtradas.map((c) => ({
return {
id: c.id,
pacienteId: c.pacienteId,
medicoId: c.medicoId,
pacienteNome: pacMap[c.pacienteId]?.nome || c.pacienteId,
pacienteNome,
medicoNome: medicoNome,
dataHora: c.dataHora,
status: c.status,
tipo: c.tipo,
observacoes: c.observacoes,
}))
};
})
);
setConsultas(consultasComNomes);
} else {
setConsultas([]);
}
} catch (error) {
console.error("Erro ao buscar consultas:", error);
toast.error("Erro ao carregar consultas");
setConsultas([]);
} finally {
setLoading(false);
}
}, [medicoId, filtroData, medicoNome]);
}, [user, roles, medicoId, medicoNome]);
useEffect(() => {
fetchConsultas();
@ -334,9 +375,8 @@ const PainelMedico: React.FC = () => {
};
// Stats
const consultasHoje = consultas.filter((c) =>
c.dataHora.startsWith(format(new Date(), "yyyy-MM-dd"))
);
// Exibe todas as consultas do médico logado, sem filtro extra
const consultasHoje = consultas;
const consultasConfirmadas = consultas.filter(
(c) =>
c.status.toLowerCase() === "confirmada" ||
@ -363,7 +403,15 @@ const PainelMedico: React.FC = () => {
{/* Doctor Profile */}
<div className="p-6 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-lg">
<div className="relative group">
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="h-14 w-14 rounded-full object-cover border shadow"
/>
) : (
<div className="h-14 w-14 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-lg shadow">
{medicoNome
.split(" ")
.map((n) => n[0])
@ -371,6 +419,38 @@ const PainelMedico: React.FC = () => {
.toUpperCase()
.slice(0, 2)}
</div>
)}
<button
type="button"
className="absolute bottom-0 right-0 bg-white rounded-full p-1 border shadow group-hover:bg-indigo-100 transition"
title="Editar avatar"
onClick={() => setAvatarEditMode(true)}
style={{ lineHeight: 0 }}
>
<Pencil size={16} className="text-indigo-600" />
</button>
{avatarEditMode && (
<form
className="absolute top-0 left-16 bg-white p-2 rounded shadow z-10 flex flex-col items-center"
onSubmit={e => {
e.preventDefault();
handleAvatarUpload();
}}
>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={e => setAvatarFile(e.target.files?.[0] || null)}
className="mb-2"
/>
<button type="submit" className="text-xs bg-indigo-600 text-white px-2 py-1 rounded">Salvar</button>
<button type="button" className="text-xs ml-2" onClick={() => setAvatarEditMode(false)}>Cancelar</button>
{avatarUrl && (
<button type="button" className="text-xs text-red-600 underline mt-2" onClick={handleAvatarRemove}>Remover</button>
)}
</form>
)}
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">
{medicoNome}
@ -848,27 +928,15 @@ const PainelMedico: React.FC = () => {
{/* Modals */}
{modalOpen && (
<ConsultaModal
open={modalOpen}
isOpen={modalOpen}
onClose={() => {
setModalOpen(false);
setEditing(null);
}}
onSave={handleSaveConsulta}
initialData={
editing
? {
id: editing.id,
pacienteId: editing.pacienteId,
medicoId: editing.medicoId,
dataHora: editing.dataHora,
status: editing.status,
tipo: editing.tipo,
observacoes: editing.observacoes,
}
: undefined
}
doctorId={medicoId}
doctorName={medicoNome}
onSaved={handleSaveConsulta}
editing={editing}
defaultMedicoId={medicoId}
lockMedico={false}
/>
)}

View File

@ -1483,35 +1483,35 @@ const PainelSecretaria = () => {
}
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/70 shadow-sm border-b">
<div className="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 py-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 high-contrast:bg-black">
<header className="bg-white/90 dark:bg-gray-800/90 high-contrast:bg-black shadow-sm border-b dark:border-gray-700 high-contrast:border-gray-700">
<div className="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 py-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between high-contrast:text-white">
<div>
<h1 className="text-2xl font-bold text-gray-900">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white high-contrast:text-black high-contrast:font-extrabold">
Painel da Secretária
</h1>
<p className="text-gray-600">
<p className="text-gray-600 dark:text-white high-contrast:text-black high-contrast:font-bold">
Gerencie pacientes, médicos e consultas
</p>
</div>
<div className="flex flex-wrap gap-3">
<button
onClick={openCreatePacienteModal}
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-green-500"
className="bg-green-600 text-white high-contrast:bg-black high-contrast:text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-green-500"
>
<Plus className="w-5 h-5 mr-2" />
Novo Paciente
</button>
<button
onClick={openCreateMedicoModal}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
className="bg-blue-600 text-white high-contrast:bg-black high-contrast:text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
>
<UserPlus className="w-5 h-5 mr-2" />
Novo Médico
</button>
<button
onClick={handleLogout}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-red-500"
className="bg-red-600 text-white high-contrast:bg-black high-contrast:text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-red-500"
>
Sair
</button>
@ -1519,7 +1519,7 @@ const PainelSecretaria = () => {
</div>
</header>
<nav className="bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/70 border-b">
<nav className="bg-white/90 dark:bg-gray-800/90 high-contrast:bg-black border-b dark:border-gray-700 high-contrast:border-gray-700">
<div className="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex space-x-2 sm:space-x-4 overflow-x-auto">
{[
@ -1542,8 +1542,8 @@ const PainelSecretaria = () => {
onClick={() => setActiveTab(tab.id)}
className={`flex items-center px-3 py-4 border-b-2 font-medium text-sm whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500/50 ${
selected
? "border-green-500 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
? "border-green-500 text-green-600 high-contrast:text-white"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 high-contrast:text-white"
}`}
>
<Icon className="w-5 h-5 mr-2" />
@ -1558,7 +1558,7 @@ const PainelSecretaria = () => {
<main className="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{activeTab === "dashboard" && (
<section className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow border dark:border-gray-700">
<p className="text-sm font-medium text-gray-600">
Pacientes cadastrados
</p>
@ -1566,7 +1566,7 @@ const PainelSecretaria = () => {
{pacientes.length}
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow border dark:border-gray-700">
<p className="text-sm font-medium text-gray-600">
Médicos ativos
</p>
@ -1574,7 +1574,7 @@ const PainelSecretaria = () => {
{medicos.length}
</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow border dark:border-gray-700">
<p className="text-sm font-medium text-gray-600">
Consultas programadas
</p>
@ -1587,8 +1587,8 @@ const PainelSecretaria = () => {
{activeTab === "pacientes" && (
<section className="space-y-6">
<div className="bg-white rounded-xl shadow border border-gray-200">
<div className="p-6 border-b space-y-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow border border-gray-200 dark:border-gray-700">
<div className="p-6 border-b border-gray-200 dark:border-gray-700 space-y-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
@ -1597,7 +1597,7 @@ const PainelSecretaria = () => {
placeholder="Buscar pacientes por nome ou email..."
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
className="pl-10 pr-4 py-2 w-full border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
/>
</div>
<div className="flex flex-col md:flex-row gap-2">
@ -1606,7 +1606,7 @@ const PainelSecretaria = () => {
placeholder="Buscar paciente por ID"
value={searchId}
onChange={(event) => setSearchId(event.target.value)}
className="border px-3 py-2 rounded-lg w/full md:w-64 focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
className="border px-3 py-2 rounded-lg w/full md:w-64 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
/>
<div className="flex gap-2">
<button
@ -1624,7 +1624,7 @@ const PainelSecretaria = () => {
setFilterVip(false);
void carregarPacientes();
}}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300"
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 dark:focus-visible:ring-gray-700"
>
Limpar
</button>
@ -1668,7 +1668,7 @@ const PainelSecretaria = () => {
onChange={(event) =>
setSelectedConvenio(event.target.value)
}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
>
<option value="">Todos</option>
{conveniosDisponiveis.map((item) => (
@ -1724,7 +1724,7 @@ const PainelSecretaria = () => {
{activeTab === "medicos" && (
<section className="space-y-6">
<div className="bg-white rounded-lg shadow">
<div className="bg-white dark:bg-gray-800 high-contrast:bg-black rounded-lg shadow border dark:border-gray-700 high-contrast:border-gray-700">
<div className="p-6 border-b flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
@ -1739,8 +1739,8 @@ const PainelSecretaria = () => {
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="sticky top-0 z-10 bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
<thead className="sticky top-0 z-10 bg-gray-50 dark:bg-gray-900 high-contrast:bg-black">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Médico
@ -1756,28 +1756,28 @@ const PainelSecretaria = () => {
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 high-contrast:bg-black divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
{medicosFiltrados.map((medico) => (
<tr
key={medico.id}
className="odd:bg-white even:bg-gray-50 hover:bg-gray-100"
className="bg-white dark:bg-gray-800 high-contrast:bg-black hover:bg-gray-100 dark:hover:bg-gray-700 high-contrast:hover:bg-gray-900"
>
<td className="px-6 py-4">
<div className="text-sm font-medium text-gray-900">
<td className="px-6 py-4 high-contrast:text-white">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 high-contrast:text-white">
Dr(a). {medico.nome || "Sem nome"}
</div>
<div className="text-sm text-gray-500">
<div className="text-sm text-gray-400 dark:text-gray-300 high-contrast:text-white">
CRM: {medico.crm || "Não informado"}
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500">
<td className="px-6 py-4 text-sm text-gray-400 dark:text-gray-300 high-contrast:text-white">
{medico.especialidade || "Não informado"}
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
<td className="px-6 py-4 high-contrast:text-white">
<div className="text-sm text-gray-900 dark:text-gray-100 high-contrast:text-white">
{formatEmail(medico.email)}
</div>
<div className="text-sm text-gray-500">
<div className="text-sm text-gray-400 dark:text-gray-300 high-contrast:text-white">
{medico.telefone || "Telefone não informado"}
</div>
</td>
@ -1815,8 +1815,8 @@ const PainelSecretaria = () => {
)}
{activeTab === "consultas" && (
<section className="bg-white rounded-lg shadow">
<div className="p-6 border-b flex flex-col gap-4">
<section className="bg-white dark:bg-gray-800 high-contrast:bg-black rounded-lg shadow border border-gray-200 dark:border-gray-700 high-contrast:border-gray-700">
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
Agendamentos
@ -1849,7 +1849,7 @@ const PainelSecretaria = () => {
placeholder="Busca rápida (paciente, médico ou tipo)"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-3">
@ -1861,7 +1861,7 @@ const PainelSecretaria = () => {
type="date"
value={consultaFiltroDataDe}
onChange={(e) => setConsultaFiltroDataDe(e.target.value)}
className="w-full border rounded px-2 py-1 text-sm"
className="w-full border border-gray-300 dark:border-gray-700 rounded px-2 py-1 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
@ -1872,7 +1872,7 @@ const PainelSecretaria = () => {
type="date"
value={consultaFiltroDataAte}
onChange={(e) => setConsultaFiltroDataAte(e.target.value)}
className="w-full border rounded px-2 py-1 text-sm"
className="w-full border border-gray-300 dark:border-gray-700 rounded px-2 py-1 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
@ -1882,7 +1882,7 @@ const PainelSecretaria = () => {
<select
value={consultaFiltroStatus}
onChange={(e) => setConsultaFiltroStatus(e.target.value)}
className="w-full border rounded px-2 py-1 text-sm"
className="w-full border border-gray-300 dark:border-gray-700 rounded px-2 py-1 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
>
<option value="">Todos</option>
<option value="agendada">Agendada</option>
@ -1898,7 +1898,7 @@ const PainelSecretaria = () => {
<input
value={consultaFiltroPaciente}
onChange={(e) => setConsultaFiltroPaciente(e.target.value)}
className="w-full border rounded px-2 py-1 text-sm"
className="w-full border border-gray-300 dark:border-gray-700 rounded px-2 py-1 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
placeholder="Filtrar paciente"
/>
</div>
@ -1909,7 +1909,7 @@ const PainelSecretaria = () => {
<input
value={consultaFiltroMedico}
onChange={(e) => setConsultaFiltroMedico(e.target.value)}
className="w-full border rounded px-2 py-1 text-sm"
className="w-full border border-gray-300 dark:border-gray-700 rounded px-2 py-1 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
placeholder="Filtrar médico"
/>
</div>
@ -1927,8 +1927,8 @@ const PainelSecretaria = () => {
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="sticky top-0 z-10 bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
<thead className="sticky top-0 z-10 bg-gray-50 dark:bg-gray-900 high-contrast:bg-black">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data/Hora
@ -1950,22 +1950,22 @@ const PainelSecretaria = () => {
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 high-contrast:bg-black divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
{consultasFiltradas.map((consulta) => (
<tr
key={consulta.id}
className="odd:bg-white even:bg-gray-50 hover:bg-gray-100"
className="bg-white dark:bg-gray-800 high-contrast:bg-black hover:bg-gray-100 dark:hover:bg-gray-700 high-contrast:hover:bg-gray-900"
>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{formatDateTimeLocal(consulta.dataHora)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{consulta.pacienteNome}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{consulta.medicoNome}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{consulta.tipo}
</td>
<td className="px-6 py-4 whitespace-nowrap">
@ -1977,7 +1977,7 @@ const PainelSecretaria = () => {
e.target.value
)
}
className="text-sm border rounded px-2 py-1"
className="text-sm border border-gray-300 dark:border-gray-700 high-contrast:border-gray-700 rounded px-2 py-1 bg-white dark:bg-gray-900 high-contrast:bg-black text-gray-900 high-contrast:text-white"
>
<option value="agendada">Agendada</option>
<option value="confirmada">Confirmada</option>
@ -2045,7 +2045,7 @@ const PainelSecretaria = () => {
)}
{activeTab === "relatorios" && (
<section className="bg-white rounded-lg shadow">
<section className="bg-white dark:bg-gray-800 high-contrast:bg-black rounded-lg shadow border border-gray-200 dark:border-gray-700 high-contrast:border-gray-700">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-semibold text-gray-900">
@ -2083,8 +2083,8 @@ const PainelSecretaria = () => {
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="sticky top-0 z-10 bg-gray-50">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
<thead className="sticky top-0 z-10 bg-gray-50 dark:bg-gray-900 high-contrast:bg-black">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Número
@ -2106,19 +2106,19 @@ const PainelSecretaria = () => {
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-gray-800 high-contrast:bg-black divide-y divide-gray-200 dark:divide-gray-700 high-contrast:divide-gray-700">
{relatorios.map((relatorio) => (
<tr
key={relatorio.id}
className="odd:bg-white even:bg-gray-50 hover:bg-gray-100"
className="bg-white dark:bg-gray-800 high-contrast:bg-black hover:bg-gray-100 dark:hover:bg-gray-700 high-contrast:hover:bg-gray-900"
>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 high-contrast:text-white">
{relatorio.order_number}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{relatorio.exam}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 high-contrast:text-white">
{pacientes.find(
(p) => p.id === relatorio.patient_id
)?.nome || relatorio.patient_id}
@ -2127,12 +2127,12 @@ const PainelSecretaria = () => {
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
relatorio.status === "draft"
? "bg-gray-100 text-gray-800"
? "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 high-contrast:bg-gray-900 high-contrast:text-white"
: relatorio.status === "completed"
? "bg-green-100 text-green-800"
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 high-contrast:bg-green-900 high-contrast:text-white"
: relatorio.status === "pending"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 high-contrast:bg-yellow-900 high-contrast:text-white"
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 high-contrast:bg-red-900 high-contrast:text-white"
}`}
>
{relatorio.status === "draft"

View File

@ -400,6 +400,65 @@ class AvailabilityService {
});
return { success: true, data: summary };
}
// Compatibilidade: método utilitário para retornar a disponibilidade semanal
// no formato ApiResponse esperado pelos componentes existentes.
async getAvailability(
doctorId: string
): Promise<
ApiResponse<any>
> {
try {
// Alguns componentes esperam a disponibilidade semanal (com chaves
// domingo/segunda/..). Tentar obter via listAvailability (tabelas
// granular por weekday) e transformar se necessário.
const res = await this.listAvailability({ doctor_id: doctorId });
if (!res.success || !res.data) {
return { success: false, error: res.error || "Nenhuma disponibilidade" };
}
// Se o backend já retornar o objeto semanal (com campos domingo..sabado),
// apenas repassar. Caso contrário, agrupar os registros por weekday
// para manter compatibilidade com componentes que esperam esse formato.
const first = res.data[0];
const looksLikeWeekly = first && (first as any).domingo !== undefined;
if (looksLikeWeekly) {
return { success: true, data: res.data };
}
// Agrupar registros por weekday (convertido para PT-BR em listAvailability)
const weekly: Record<string, any> = {
domingo: { ativo: false, horarios: [] },
segunda: { ativo: false, horarios: [] },
terca: { ativo: false, horarios: [] },
quarta: { ativo: false, horarios: [] },
quinta: { ativo: false, horarios: [] },
sexta: { ativo: false, horarios: [] },
sabado: { ativo: false, horarios: [] },
};
res.data.forEach((item: any) => {
const wd = item.weekday as any; // segunda/terca/...
if (!wd) return;
// Converter cada registro para formato { ativo, horarios: [{inicio,fim,ativo}] }
const entry = {
ativo: item.active ?? item.ativo ?? true,
horarios: [
{
inicio: item.start_time || item.inicio || "",
fim: item.end_time || item.fim || "",
ativo: item.active ?? item.ativo ?? true,
},
],
};
weekly[wd] = entry;
});
return { success: true, data: [weekly] };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Erro desconhecido" };
}
}
}
export const availabilityService = new AvailabilityService();

View File

@ -67,6 +67,20 @@ interface RawConsulta {
}
class ConsultasService {
async listarTodas(): Promise<ApiResponse<Consulta[]>> {
try {
const response = await api.get(ENDPOINTS.CONSULTATIONS, {
params: { select: "*" },
});
const data: RawConsulta[] = Array.isArray(response.data)
? response.data
: response.data?.data || [];
return { success: true, data: data.map(this.mapConsulta) };
} catch (error) {
console.error("Erro ao listar todas as consultas:", error);
return { success: false, error: "Erro ao listar todas as consultas" };
}
}
async listarPorPaciente(
pacienteId: string,
params?: { futureOnly?: boolean; limit?: number; sort?: "asc" | "desc" }

View File

@ -37,6 +37,7 @@ export interface Paciente {
numeroCarteirinha?: string;
observacoes?: string | null;
vip?: boolean; // derivado de flags/tags legado
avatar_url?: string;
}
export interface Pagination {
@ -86,6 +87,7 @@ interface PacienteApi extends Partial<PatientSchema> {
vip?: boolean | string | number;
is_vip?: boolean | string | number;
vip_status?: boolean | string | number;
avatar_url?: string;
}
const TRUTHY_STRINGS = new Set(["true", "1", "yes", "sim", "vip", "ativo"]);
@ -154,6 +156,7 @@ function mapPacienteFromApi(p: PacienteApi): Paciente {
numeroCarteirinha: p.numeroCarteirinha || p.numero_carteirinha,
observacoes: p.observacoes || null,
vip: extractVipFlag(p),
avatar_url: p.avatar_url,
};
}