diff --git a/susconecta/Documentação API.md b/susconecta/Documentação API.md deleted file mode 100644 index b1b9604..0000000 --- a/susconecta/Documentação API.md +++ /dev/null @@ -1,2565 +0,0 @@ -# Listar atribuições de pacientes - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patient_assignments: - get: - summary: Listar atribuições de pacientes - deprecated: false - description: '' - tags: - - Atribuições - - Atribuições - parameters: - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - responses: - '200': - description: Lista de atribuições - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/PatientAssignment' - headers: {} - x-apidog-name: OK - security: - - bearer: [] - x-apidog-folder: Atribuições - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940525-run -components: - schemas: - PatientAssignment: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - patient_id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - user_id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - role: - type: string - enum: - - medico - - enfermeiro - examples: - - medico - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - patient_id - - user_id - - role - - created_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Criar nova atribuição - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patient_assignments: - post: - summary: Criar nova atribuição - deprecated: false - description: '' - tags: - - Atribuições - - Atribuições - parameters: - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatientAssignmentInput' - responses: - '201': - description: Atribuição criada - content: - application/json: - schema: - $ref: '#/components/schemas/PatientAssignment' - headers: {} - x-apidog-name: Created - security: - - bearer: [] - x-apidog-folder: Atribuições - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940526-run -components: - schemas: - PatientAssignmentInput: - type: object - required: - - patient_id - - user_id - - role - properties: - patient_id: - type: string - format: uuid - user_id: - type: string - format: uuid - role: - type: string - enum: - - medico - - enfermeiro - x-apidog-orders: - - patient_id - - user_id - - role - x-apidog-ignore-properties: [] - x-apidog-folder: '' - PatientAssignment: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - patient_id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - user_id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - role: - type: string - enum: - - medico - - enfermeiro - examples: - - medico - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - patient_id - - user_id - - role - - created_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Listar roles de usuários - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/user_roles: - get: - summary: Listar roles de usuários - deprecated: false - description: '' - tags: - - Usuários - - Usuários - parameters: [] - responses: - '200': - description: Lista de roles - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/UserRole' - headers: {} - x-apidog-name: OK - security: - - bearer: [] - x-apidog-folder: Usuários - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940524-run -components: - schemas: - UserRole: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - user_id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - role: - type: string - enum: - - admin - - gestor - - medico - examples: - - medico - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - x-apidog-orders: - - id - - user_id - - role - - created_at - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Obter informações completas do usuário - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /functions/v1/user-info: - get: - summary: Obter informações completas do usuário - deprecated: false - description: >- - Retorna dados consolidados do usuário autenticado, incluindo perfil e - roles para controle de permissões. - tags: - - Usuários - parameters: [] - responses: - '200': - description: Informações do usuário retornadas com sucesso - content: - application/json: - schema: - $ref: '#/components/schemas/UserInfoResponse' - headers: {} - x-apidog-name: OK - '401': - description: Token inválido ou expirado - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - '500': - description: Erro interno do servidor - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Internal Server Error - security: [] - x-apidog-folder: Usuários - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21952675-run -components: - schemas: - UserInfoResponse: - type: object - properties: - user: - $ref: '#/components/schemas/User' - profile: - $ref: '#/components/schemas/Profile' - roles: - type: array - items: - type: string - enum: - - admin - - gestor - - medico - - secretaria - - user - examples: - - - medico - - admin - permissions: - $ref: '#/components/schemas/Permissions' - x-apidog-orders: - - user - - profile - - roles - - permissions - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Permissions: - type: object - properties: - isAdmin: - type: boolean - description: Se o usuário tem role de admin - examples: - - true - isManager: - type: boolean - description: Se o usuário tem role de gestor - examples: - - false - isDoctor: - type: boolean - description: Se o usuário tem role de médico - examples: - - true - isSecretary: - type: boolean - description: Se o usuário tem role de secretária - examples: - - false - isAdminOrManager: - type: boolean - description: Se o usuário é admin ou gestor (para controle de permissões) - examples: - - true - x-apidog-orders: - - isAdmin - - isManager - - isDoctor - - isSecretary - - isAdminOrManager - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Profile: - type: object - properties: - id: - type: string - format: uuid - full_name: - type: string - examples: - - Dr. João Silva - nullable: true - email: - type: string - format: email - nullable: true - phone: - type: string - examples: - - '+5511999999999' - nullable: true - avatar_url: - type: string - format: uri - nullable: true - disabled: - type: boolean - examples: - - false - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - x-apidog-orders: - - id - - full_name - - email - - phone - - avatar_url - - disabled - - created_at - - updated_at - x-apidog-ignore-properties: [] - nullable: true - x-apidog-folder: '' - User: - type: object - properties: - id: - type: string - format: uuid - examples: - - 550e8400-e29b-41d4-a716-446655440000 - email: - type: string - format: email - examples: - - usuario@exemplo.com - email_confirmed_at: - type: string - format: date-time - examples: - - '2024-01-01T10:00:00Z' - nullable: true - created_at: - type: string - format: date-time - examples: - - '2024-01-01T00:00:00Z' - last_sign_in_at: - type: string - format: date-time - examples: - - '2024-01-15T09:30:00Z' - nullable: true - x-apidog-orders: - - id - - email - - email_confirmed_at - - created_at - - last_sign_in_at - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Criar novo usuário - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /functions/v1/create-user: - post: - summary: Criar novo usuário - deprecated: false - description: >- - Cria um novo usuário no sistema com papel específico. Apenas usuários - com papel de admin, gestor ou secretaria podem criar novos usuários. - operationId: createUser - tags: - - Usuários - - Usuários - parameters: [] - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserRequest' - examples: - admin_user: - value: - email: admin@mediconnect.com - password: senha123! - full_name: João Silva - phone: (11) 99999-9999 - role: admin - summary: Criar administrador - doctor_user: - value: - email: dr.maria@mediconnect.com - password: senha123! - full_name: Dra. Maria Santos - phone: (11) 98888-8888 - role: medico - summary: Criar médico - secretary_user: - value: - email: secretaria@mediconnect.com - password: senha123! - full_name: Ana Costa - phone: (11) 97777-7777 - role: secretaria - summary: Criar secretária - responses: - '200': - description: Usuário criado com sucesso - content: - application/json: - schema: - $ref: '#/components/schemas/CreateUserResponse' - example: - success: true - user: - id: 123e4567-e89b-12d3-a456-426614174000 - email: novo.usuario@mediconnect.com - full_name: Novo Usuário - phone: (11) 99999-9999 - role: medico - headers: {} - x-apidog-name: OK - '400': - description: Dados inválidos ou erro de validação - content: - application/json: - schema: - type: object - properties: {} - x-apidog-ignore-properties: [] - x-apidog-orders: [] - examples: - '2': - summary: Campos obrigatórios faltando - value: - error: 'Missing required fields: email, password, full_name, role' - '3': - summary: Papel inválido - value: - error: Invalid role - '4': - summary: Email já existe - value: - error: User with this email already registered - headers: {} - x-apidog-name: Bad Request - '401': - description: Token de autenticação inválido ou ausente - content: - application/json: - schema: - type: object - properties: {} - x-apidog-ignore-properties: [] - x-apidog-orders: [] - example: - error: Unauthorized - headers: {} - x-apidog-name: Unauthorized - '403': - description: Permissões insuficientes - content: - application/json: - schema: - type: object - properties: {} - x-apidog-ignore-properties: [] - x-apidog-orders: [] - example: - error: Insufficient permissions - headers: {} - x-apidog-name: Forbidden - '500': - description: Erro interno do servidor - content: - application/json: - schema: - type: object - properties: {} - x-apidog-ignore-properties: [] - x-apidog-orders: [] - example: - error: Internal server error - headers: {} - x-apidog-name: Internal Server Error - security: [] - x-apidog-folder: Usuários - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21953135-run -components: - schemas: - CreateUserRequest: - type: object - required: - - email - - password - - full_name - - role - properties: - email: - type: string - format: email - description: Email do usuário (deve ser único) - examples: - - usuario@mediconnect.com - password: - type: string - minLength: 6 - description: Senha temporária para o usuário - examples: - - senha123! - full_name: - type: string - minLength: 1 - description: Nome completo do usuário - examples: - - João da Silva - phone: - type: string - description: Telefone do usuário (opcional) - examples: - - (11) 99999-9999 - nullable: true - role: - type: string - enum: - - admin - - gestor - - medico - - secretaria - - user - description: Papel do usuário no sistema - examples: - - medico - x-apidog-orders: - - email - - password - - full_name - - phone - - role - x-apidog-ignore-properties: [] - x-apidog-folder: '' - CreateUserResponse: - type: object - properties: - success: - type: boolean - description: Indica se a operação foi bem-sucedida - examples: - - true - user: - type: object - properties: - id: - type: string - format: uuid - description: ID único do usuário criado - examples: - - 123e4567-e89b-12d3-a456-426614174000 - email: - type: string - format: email - description: Email do usuário - examples: - - usuario@mediconnect.com - full_name: - type: string - description: Nome completo do usuário - examples: - - João da Silva - phone: - type: string - description: Telefone do usuário - examples: - - (11) 99999-9999 - nullable: true - role: - type: string - description: Papel atribuído ao usuário - examples: - - medico - x-apidog-orders: - - id - - email - - full_name - - phone - - role - x-apidog-ignore-properties: [] - x-apidog-orders: - - success - - user - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Obter dados do usuário atual - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /auth/v1/user: - get: - summary: Obter dados do usuário atual - deprecated: false - description: Retorna informações do usuário autenticado - tags: - - Usuários - - Authentication - parameters: [] - responses: - '200': - description: Dados do usuário - content: - application/json: - schema: - $ref: '#/components/schemas/User' - headers: {} - x-apidog-name: OK - '401': - description: Token inválido - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - security: - - bearer: [] - x-apidog-folder: Usuários - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940512-run -components: - schemas: - User: - type: object - properties: - id: - type: string - format: uuid - examples: - - 550e8400-e29b-41d4-a716-446655440000 - email: - type: string - format: email - examples: - - usuario@exemplo.com - email_confirmed_at: - type: string - format: date-time - examples: - - '2024-01-01T10:00:00Z' - nullable: true - created_at: - type: string - format: date-time - examples: - - '2024-01-01T00:00:00Z' - last_sign_in_at: - type: string - format: date-time - examples: - - '2024-01-15T09:30:00Z' - nullable: true - x-apidog-orders: - - id - - email - - email_confirmed_at - - created_at - - last_sign_in_at - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` -# Fazer login e obter token JWT - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /auth/v1/token: - post: - summary: Fazer login e obter token JWT - deprecated: false - description: >- - Autentica o usuário e retorna um token JWT para usar em outras - requisições. - tags: - - Authentication - parameters: - - name: grant_type - in: query - description: '' - required: true - schema: - type: string - enum: - - password - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/LoginRequest' - examples: {} - responses: - '200': - description: Login realizado com sucesso - content: - application/json: - schema: - $ref: '#/components/schemas/LoginResponse' - headers: {} - x-apidog-name: OK - '400': - description: Credenciais inválidas - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Bad Request - '401': - description: Email ou senha incorretos - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Unauthorized - security: - - bearer: [] - x-apidog-folder: Authentication - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940510-run -components: - schemas: - LoginRequest: - type: object - required: - - email - - password - properties: - email: - type: string - format: email - examples: - - usuario@exemplo.com - password: - type: string - minLength: 6 - examples: - - senha123 - x-apidog-orders: - - email - - password - x-apidog-ignore-properties: [] - x-apidog-folder: '' - LoginResponse: - type: object - properties: - access_token: - type: string - description: Token JWT para autenticação - examples: - - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - token_type: - type: string - examples: - - bearer - expires_in: - type: integer - description: Tempo de expiração do token em segundos - examples: - - 3600 - refresh_token: - type: string - description: Token para renovar o access_token - user: - $ref: '#/components/schemas/AuthUser' - x-apidog-orders: - - access_token - - token_type - - expires_in - - refresh_token - - user - x-apidog-ignore-properties: [] - x-apidog-folder: '' - AuthUser: - type: object - properties: - id: - type: string - format: uuid - email: - type: string - format: email - email_confirmed_at: - type: string - format: date-time - nullable: true - created_at: - type: string - format: date-time - x-apidog-orders: - - id - - email - - email_confirmed_at - - created_at - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Logout do usuário - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /auth/v1/logout: - post: - summary: Logout do usuário - deprecated: false - description: Encerrar sessão do usuário - tags: - - Authentication - - Authentication - parameters: [] - responses: - '204': - description: Logout realizado com sucesso - headers: {} - x-apidog-name: No Content - '401': - description: Token inválido - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - security: - - bearer: [] - x-apidog-folder: Authentication - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940511-run -components: - schemas: - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Listar perfis de usuários - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/profiles: - get: - summary: Listar perfis de usuários - deprecated: false - description: '' - tags: - - Perfis - - Perfis - parameters: - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - responses: - '200': - description: Lista de perfis - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Profile' - headers: {} - x-apidog-name: OK - security: - - bearer: [] - x-apidog-folder: Perfis - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940522-run -components: - schemas: - Profile: - type: object - properties: - id: - type: string - format: uuid - full_name: - type: string - examples: - - Dr. João Silva - nullable: true - email: - type: string - format: email - nullable: true - phone: - type: string - examples: - - '+5511999999999' - nullable: true - avatar_url: - type: string - format: uri - nullable: true - disabled: - type: boolean - examples: - - false - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - x-apidog-orders: - - id - - full_name - - email - - phone - - avatar_url - - disabled - - created_at - - updated_at - x-apidog-ignore-properties: [] - nullable: true - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Atualizar perfil do usuário - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/profiles: - patch: - summary: Atualizar perfil do usuário - deprecated: false - description: '' - tags: - - Perfis - - Perfis - parameters: - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileInput' - responses: - '200': - description: Perfil atualizado - content: - application/json: - schema: - $ref: '#/components/schemas/Profile' - headers: {} - x-apidog-name: OK - security: - - bearer: [] - x-apidog-folder: Perfis - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940523-run -components: - schemas: - ProfileInput: - type: object - properties: - full_name: - type: string - avatar_url: - type: string - phone: - type: string - x-apidog-orders: - - full_name - - avatar_url - - phone - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Profile: - type: object - properties: - id: - type: string - format: uuid - full_name: - type: string - examples: - - Dr. João Silva - nullable: true - email: - type: string - format: email - nullable: true - phone: - type: string - examples: - - '+5511999999999' - nullable: true - avatar_url: - type: string - format: uri - nullable: true - disabled: - type: boolean - examples: - - false - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - x-apidog-orders: - - id - - full_name - - email - - phone - - avatar_url - - disabled - - created_at - - updated_at - x-apidog-ignore-properties: [] - nullable: true - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Obter paciente por ID - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patients/{id}: - get: - summary: Obter paciente por ID - deprecated: false - description: Retorna dados de um paciente específico - tags: - - Pacientes - - Pacientes - parameters: - - name: id - in: path - description: ID do paciente - required: true - example: '' - schema: - type: string - format: uuid - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - responses: - '200': - description: Dados do paciente - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - headers: {} - x-apidog-name: OK - '401': - description: Não autorizado - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - '404': - description: Paciente não encontrado - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Not Found - security: - - bearer: [] - x-apidog-folder: Pacientes - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940515-run -components: - schemas: - Patient: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - examples: - - Maria Santos - sex: - type: string - examples: - - F - blood_type: - type: string - examples: - - A+ - weight_kg: - type: number - examples: - - 65.5 - height_m: - type: number - examples: - - 1.65 - bmi: - type: number - examples: - - 24.1 - street: - type: string - examples: - - Rua das Flores, 123 - number: - type: string - examples: - - '123' - complement: - type: string - examples: - - Apt 45 - neighborhood: - type: string - examples: - - Centro - city: - type: string - examples: - - São Paulo - state: - type: string - examples: - - SP - cep: - type: string - examples: - - 01234-567 - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - updated_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - bmi - - street - - number - - complement - - neighborhood - - city - - state - - cep - - created_at - - updated_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Atualizar paciente - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patients/{id}: - patch: - summary: Atualizar paciente - deprecated: false - description: Atualizar dados de um paciente existente - tags: - - Pacientes - - Pacientes - parameters: - - name: id - in: path - description: ID do paciente - required: true - example: '' - schema: - type: string - format: uuid - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatientInput' - responses: - '200': - description: Paciente atualizado com sucesso - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - headers: {} - x-apidog-name: OK - '401': - description: Não autorizado - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - '404': - description: Paciente não encontrado - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Not Found - security: - - bearer: [] - x-apidog-folder: Pacientes - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940516-run -components: - schemas: - PatientInput: - type: object - required: - - full_name - - cpf - - email - - phone_mobile - properties: - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - sex: - type: string - examples: - - F - blood_type: - type: string - weight_kg: - type: number - height_m: - type: number - street: - type: string - number: - type: string - complement: - type: string - neighborhood: - type: string - city: - type: string - state: - type: string - cep: - type: string - x-apidog-orders: - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - street - - number - - complement - - neighborhood - - city - - state - - cep - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Patient: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - examples: - - Maria Santos - sex: - type: string - examples: - - F - blood_type: - type: string - examples: - - A+ - weight_kg: - type: number - examples: - - 65.5 - height_m: - type: number - examples: - - 1.65 - bmi: - type: number - examples: - - 24.1 - street: - type: string - examples: - - Rua das Flores, 123 - number: - type: string - examples: - - '123' - complement: - type: string - examples: - - Apt 45 - neighborhood: - type: string - examples: - - Centro - city: - type: string - examples: - - São Paulo - state: - type: string - examples: - - SP - cep: - type: string - examples: - - 01234-567 - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - updated_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - bmi - - street - - number - - complement - - neighborhood - - city - - state - - cep - - created_at - - updated_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Deletar paciente - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patients/{id}: - delete: - summary: Deletar paciente - deprecated: false - description: Remover um paciente do sistema (apenas admins/gestores) - tags: - - Pacientes - - Pacientes - parameters: - - name: id - in: path - description: ID do paciente - required: true - example: '' - schema: - type: string - format: uuid - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '' - schema: - type: string - responses: - '204': - description: Paciente deletado com sucesso - headers: {} - x-apidog-name: No Content - '401': - description: Não autorizado - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - '403': - description: Sem permissão - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Forbidden - '404': - description: Paciente não encontrado - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Not Found - security: - - bearer: [] - x-apidog-folder: Pacientes - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940517-run -components: - schemas: - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Criar novo paciente - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patients: - post: - summary: Criar novo paciente - deprecated: false - description: Cadastrar um novo paciente no sistema - tags: - - Pacientes - - Pacientes - parameters: - - name: apikey - in: header - description: Chave da API Supabase - required: true - example: '{{API_KEY}}' - schema: - type: string - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatientInput' - example: - full_name: Maria Santos - cpf: '12345678901' - email: maria@email.com - phone_mobile: (11) 99999-9999 - birth_date: '1980-01-15' - responses: - '201': - description: Paciente criado com sucesso - content: - application/json: - schema: - $ref: '#/components/schemas/Patient' - headers: {} - x-apidog-name: Created - '400': - description: Dados inválidos - content: - application/json: - schema: &ref_0 - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Bad Request - '401': - description: Não autorizado - content: - application/json: - schema: *ref_0 - headers: {} - x-apidog-name: Unauthorized - security: - - bearer: [] - x-apidog-folder: Pacientes - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940514-run -components: - schemas: - PatientInput: - type: object - required: - - full_name - - cpf - - email - - phone_mobile - properties: - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - sex: - type: string - examples: - - F - blood_type: - type: string - weight_kg: - type: number - height_m: - type: number - street: - type: string - number: - type: string - complement: - type: string - neighborhood: - type: string - city: - type: string - state: - type: string - cep: - type: string - x-apidog-orders: - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - street - - number - - complement - - neighborhood - - city - - state - - cep - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Patient: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - examples: - - Maria Santos - sex: - type: string - examples: - - F - blood_type: - type: string - examples: - - A+ - weight_kg: - type: number - examples: - - 65.5 - height_m: - type: number - examples: - - 1.65 - bmi: - type: number - examples: - - 24.1 - street: - type: string - examples: - - Rua das Flores, 123 - number: - type: string - examples: - - '123' - complement: - type: string - examples: - - Apt 45 - neighborhood: - type: string - examples: - - Centro - city: - type: string - examples: - - São Paulo - state: - type: string - examples: - - SP - cep: - type: string - examples: - - 01234-567 - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - updated_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - bmi - - street - - number - - complement - - neighborhood - - city - - state - - cep - - created_at - - updated_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` - -# Listar pacientes - -## OpenAPI Specification - -```yaml -openapi: 3.0.1 -info: - title: '' - description: '' - version: 1.0.0 -paths: - /rest/v1/patients: - get: - summary: Listar pacientes - deprecated: false - description: Retorna lista de pacientes com base nas permissões do usuário - tags: - - Pacientes - - Pacientes - parameters: - - name: apikey - in: header - description: '' - required: false - example: '{{apikey}}' - schema: - type: string - responses: - '200': - description: Lista de pacientes - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Patient' - headers: {} - x-apidog-name: OK - '401': - description: Não autorizado - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - headers: {} - x-apidog-name: Unauthorized - security: - - bearer: [] - x-apidog-folder: Pacientes - x-apidog-status: released - x-run-in-apidog: https://app.apidog.com/web/project/1053378/apis/api-21940513-run -components: - schemas: - Patient: - type: object - properties: - id: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - full_name: - type: string - examples: - - Maria Santos Silva - cpf: - type: string - examples: - - '12345678901' - email: - type: string - format: email - examples: - - maria@email.com - phone_mobile: - type: string - examples: - - (11) 99999-9999 - birth_date: - type: string - format: date - examples: - - '1980-01-15' - social_name: - type: string - examples: - - Maria Santos - sex: - type: string - examples: - - F - blood_type: - type: string - examples: - - A+ - weight_kg: - type: number - examples: - - 65.5 - height_m: - type: number - examples: - - 1.65 - bmi: - type: number - examples: - - 24.1 - street: - type: string - examples: - - Rua das Flores, 123 - number: - type: string - examples: - - '123' - complement: - type: string - examples: - - Apt 45 - neighborhood: - type: string - examples: - - Centro - city: - type: string - examples: - - São Paulo - state: - type: string - examples: - - SP - cep: - type: string - examples: - - 01234-567 - created_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - updated_at: - type: string - format: date-time - examples: - - '2024-01-15T10:30:00Z' - created_by: - type: string - format: uuid - examples: - - 12345678-1234-1234-1234-123456789012 - x-apidog-orders: - - id - - full_name - - cpf - - email - - phone_mobile - - birth_date - - social_name - - sex - - blood_type - - weight_kg - - height_m - - bmi - - street - - number - - complement - - neighborhood - - city - - state - - cep - - created_at - - updated_at - - created_by - x-apidog-ignore-properties: [] - x-apidog-folder: '' - Error: - type: object - properties: - error: - type: string - message: - type: string - code: - type: string - x-apidog-orders: - - error - - message - - code - x-apidog-ignore-properties: [] - x-apidog-folder: '' - securitySchemes: - bearerAuth: - type: jwt - scheme: bearer - bearerFormat: JWT - description: Token JWT obtido no login - bearer: - type: http - scheme: bearer -servers: - - url: https://yuanqfswhberkoevtmfr.supabase.co - description: Prod Env - - url: '' - description: Cloud Mock -security: - - bearer: [] - -``` diff --git a/susconecta/app/(main-routes)/dashboard/page.tsx b/susconecta/app/(main-routes)/dashboard/page.tsx index ffd17fe..9d9c5c0 100644 --- a/susconecta/app/(main-routes)/dashboard/page.tsx +++ b/susconecta/app/(main-routes)/dashboard/page.tsx @@ -1,41 +1,403 @@ -export default function DashboardPage() { - return ( - <> -
-
-

Dashboard

-

- Bem-vindo ao painel de controle -

-
+'use client'; -
-
-

- Total de Pacientes -

-

1,234

-
-
-

- Consultas Hoje -

-

28

-
-
-

- Próximas Consultas -

-

45

-
-
-

- Receita Mensal -

-

R$ 45.230

+import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { + countTotalPatients, + countTotalDoctors, + countAppointmentsToday, + getUpcomingAppointments, + getAppointmentsByDateRange, + getNewUsersLastDays, + getPendingReports, + getDisabledUsers, + getDoctorsAvailabilityToday, + getPatientById, + getDoctorById, +} from '@/lib/api'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { AlertCircle, Calendar, Users, Stethoscope, Clock, FileText, AlertTriangle, Plus, ArrowLeft } from 'lucide-react'; +import Link from 'next/link'; +import { PatientRegistrationForm } from '@/components/forms/patient-registration-form'; +import { DoctorRegistrationForm } from '@/components/forms/doctor-registration-form'; + +interface DashboardStats { + totalPatients: number; + totalDoctors: number; + appointmentsToday: number; +} + +interface UpcomingAppointment { + id: string; + scheduled_at: string; + status: string; + doctor_id: string; + patient_id: string; + doctor?: { full_name?: string }; + patient?: { full_name?: string }; +} + +export default function DashboardPage() { + const router = useRouter(); + const [stats, setStats] = useState({ + totalPatients: 0, + totalDoctors: 0, + appointmentsToday: 0, + }); + const [appointments, setAppointments] = useState([]); + const [appointmentData, setAppointmentData] = useState([]); + const [newUsers, setNewUsers] = useState([]); + const [pendingReports, setPendingReports] = useState([]); + const [disabledUsers, setDisabledUsers] = useState([]); + const [doctors, setDoctors] = useState>(new Map()); + const [patients, setPatients] = useState>(new Map()); + const [loading, setLoading] = useState(true); + + // Estados para os modais de formulário + const [showPatientForm, setShowPatientForm] = useState(false); + const [showDoctorForm, setShowDoctorForm] = useState(false); + const [editingPatientId, setEditingPatientId] = useState(null); + const [editingDoctorId, setEditingDoctorId] = useState(null); + + useEffect(() => { + loadDashboardData(); + }, []); + + const loadDashboardData = async () => { + try { + setLoading(true); + + // 1. Carrega stats + const [patientCount, doctorCount, todayCount] = await Promise.all([ + countTotalPatients(), + countTotalDoctors(), + countAppointmentsToday(), + ]); + + setStats({ + totalPatients: patientCount, + totalDoctors: doctorCount, + appointmentsToday: todayCount, + }); + + // 2. Carrega dados dos widgets em paralelo + const [upcomingAppts, appointmentDataRange, newUsersList, pendingReportsList, disabledUsersList] = await Promise.all([ + getUpcomingAppointments(5), + getAppointmentsByDateRange(7), + getNewUsersLastDays(7), + getPendingReports(5), + getDisabledUsers(5), + ]); + + setAppointments(upcomingAppts); + setAppointmentData(appointmentDataRange); + setNewUsers(newUsersList); + setPendingReports(pendingReportsList); + setDisabledUsers(disabledUsersList); + + // 3. Busca detalhes de pacientes e médicos para as próximas consultas + const doctorMap = new Map(); + const patientMap = new Map(); + + for (const appt of upcomingAppts) { + if (appt.doctor_id && !doctorMap.has(appt.doctor_id)) { + const doctor = await getDoctorById(appt.doctor_id); + if (doctor) doctorMap.set(appt.doctor_id, doctor); + } + if (appt.patient_id && !patientMap.has(appt.patient_id)) { + const patient = await getPatientById(appt.patient_id); + if (patient) patientMap.set(appt.patient_id, patient); + } + } + + setDoctors(doctorMap); + setPatients(patientMap); + } catch (err) { + console.error('[Dashboard] Erro ao carregar dados:', err); + } finally { + setLoading(false); + } + }; + + const handlePatientFormSaved = () => { + setShowPatientForm(false); + setEditingPatientId(null); + loadDashboardData(); + }; + + const handleDoctorFormSaved = () => { + setShowDoctorForm(false); + setEditingDoctorId(null); + loadDashboardData(); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const getStatusBadge = (status: string) => { + const statusMap: Record = { + confirmed: { variant: 'default', label: 'Confirmado' }, + completed: { variant: 'secondary', label: 'Concluído' }, + cancelled: { variant: 'destructive', label: 'Cancelado' }, + requested: { variant: 'outline', label: 'Solicitado' }, + }; + const s = statusMap[status] || { variant: 'outline', label: status }; + return {s.label}; + }; + + if (loading) { + return ( +
+
+
+
+ {[1, 2, 3, 4].map(i => ( +
+ ))}
- + ); + } + + // Se está exibindo formulário de paciente + if (showPatientForm) { + return ( +
+
+ +

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

+
+ + { + setShowPatientForm(false); + setEditingPatientId(null); + }} + /> +
+ ); + } + + // Se está exibindo formulário de médico + if (showDoctorForm) { + return ( +
+
+ +

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

+
+ + { + setShowDoctorForm(false); + setEditingDoctorId(null); + }} + /> +
+ ); + } + + return ( +
+ {/* Header */} +
+

Dashboard

+

Bem-vindo ao painel de controle

+
+ + {/* 1. CARDS RESUMO */} +
+
+
+
+

Total de Pacientes

+

{stats.totalPatients}

+
+ +
+
+ +
+
+
+

Total de Médicos

+

{stats.totalDoctors}

+
+ +
+
+ +
+
+
+

Consultas Hoje

+

{stats.appointmentsToday}

+
+ +
+
+ +
+
+
+

Relatórios Pendentes

+

{pendingReports.length}

+
+ +
+
+
+ + {/* 6. AÇÕES RÁPIDAS */} +
+

Ações Rápidas

+
+ + + + +
+
+ + {/* 2. PRÓXIMAS CONSULTAS */} +
+
+

Próximas Consultas (7 dias)

+ {appointments.length > 0 ? ( +
+ {appointments.map(appt => ( +
+
+

+ {patients.get(appt.patient_id)?.full_name || 'Paciente desconhecido'} +

+

+ Médico: {doctors.get(appt.doctor_id)?.full_name || 'Médico desconhecido'} +

+

{formatDate(appt.scheduled_at)}

+
+
+ {getStatusBadge(appt.status)} +
+
+ ))} +
+ ) : ( +

Nenhuma consulta agendada para os próximos 7 dias

+ )} +
+ + {/* 5. RELATÓRIOS PENDENTES */} +
+

+ + Relatórios Pendentes +

+ {pendingReports.length > 0 ? ( +
+ {pendingReports.map(report => ( +
+

{report.order_number}

+

{report.exam || 'Sem descrição'}

+
+ ))} + +
+ ) : ( +

Sem relatórios pendentes

+ )} +
+
+ + {/* 4. NOVOS USUÁRIOS */} +
+

Novos Usuários (últimos 7 dias)

+ {newUsers.length > 0 ? ( +
+ {newUsers.map(user => ( +
+

{user.full_name || 'Sem nome'}

+

{user.email}

+
+ ))} +
+ ) : ( +

Nenhum novo usuário nos últimos 7 dias

+ )} +
+ + {/* 8. ALERTAS */} + {disabledUsers.length > 0 && ( +
+

+ + Alertas - Usuários Desabilitados +

+
+ {disabledUsers.map(user => ( + + + + {user.full_name} ({user.email}) está desabilitado + + + ))} +
+
+ )} + + {/* 11. LINK PARA RELATÓRIOS */} +
+

Seção de Relatórios

+

+ Acesse a seção de relatórios médicos para gerenciar, visualizar e exportar documentos. +

+ +
+
); } + diff --git a/susconecta/app/(main-routes)/perfil/loading.tsx b/susconecta/app/(main-routes)/perfil/loading.tsx new file mode 100644 index 0000000..9b7a1af --- /dev/null +++ b/susconecta/app/(main-routes)/perfil/loading.tsx @@ -0,0 +1,34 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function PerfillLoading() { + return ( +
+
+ +
+ + +
+
+ +
+
+ +
+ + + +
+
+ +
+ +
+ + +
+
+
+
+ ); +} diff --git a/susconecta/app/(main-routes)/perfil/page.tsx b/susconecta/app/(main-routes)/perfil/page.tsx new file mode 100644 index 0000000..2db4cf9 --- /dev/null +++ b/susconecta/app/(main-routes)/perfil/page.tsx @@ -0,0 +1,653 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { UploadAvatar } from "@/components/ui/upload-avatar"; +import { AlertCircle, ArrowLeft, CheckCircle, XCircle } from "lucide-react"; +import { getUserInfoById } from "@/lib/api"; +import { useAuth } from "@/hooks/useAuth"; +import { formatTelefone, formatCEP, validarCEP, buscarCEP } from "@/lib/utils"; + +interface UserProfile { + user: { + id: string; + email: string; + created_at: string; + last_sign_in_at: string | null; + email_confirmed_at: string | null; + }; + profile: { + id: string; + full_name: string | null; + email: string | null; + phone: string | null; + avatar_url: string | null; + cep?: string | null; + street?: string | null; + number?: string | null; + complement?: string | null; + neighborhood?: string | null; + city?: string | null; + state?: string | null; + disabled: boolean; + created_at: string; + updated_at: string; + } | null; + roles: string[]; + permissions: { + isAdmin: boolean; + isManager: boolean; + isDoctor: boolean; + isSecretary: boolean; + isAdminOrManager: boolean; + }; +} + +export default function PerfilPage() { + const router = useRouter(); + const { user: authUser } = useAuth(); + const [userInfo, setUserInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [editingData, setEditingData] = useState<{ + phone?: string; + full_name?: string; + avatar_url?: string; + cep?: string; + street?: string; + number?: string; + complement?: string; + neighborhood?: string; + city?: string; + state?: string; + }>({}); + const [cepLoading, setCepLoading] = useState(false); + const [cepValid, setCepValid] = useState(null); + + useEffect(() => { + async function loadUserInfo() { + try { + setLoading(true); + + if (!authUser?.id) { + throw new Error("ID do usuário não encontrado"); + } + + console.log('[PERFIL] Chamando getUserInfoById com ID:', authUser.id); + + // Para admin/gestor, usar getUserInfoById com o ID do usuário logado + const info = await getUserInfoById(authUser.id); + console.log('[PERFIL] Sucesso ao carregar info:', info); + setUserInfo(info as UserProfile); + setError(null); + } catch (err: any) { + console.error('[PERFIL] Erro ao carregar:', err); + setError(err?.message || "Erro ao carregar informações do perfil"); + setUserInfo(null); + } finally { + setLoading(false); + } + } + + if (authUser) { + console.log('[PERFIL] useEffect acionado, authUser:', authUser); + loadUserInfo(); + } + }, [authUser]); + + if (authUser?.userType !== 'administrador') { + return ( +
+
+ + + + Você não tem permissão para acessar esta página. + + + +
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+
+ + + {error} + + +
+
+ ); + } + + if (!userInfo) { + return ( +
+
+ + + + Nenhuma informação de perfil disponível. + + +
+
+ ); + } + + const getInitials = (name: string | null | undefined) => { + if (!name) return "AD"; + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + const handleEditClick = () => { + if (!isEditing && userInfo) { + setEditingData({ + full_name: userInfo.profile?.full_name || "", + phone: userInfo.profile?.phone || "", + avatar_url: userInfo.profile?.avatar_url || "", + cep: userInfo.profile?.cep || "", + street: userInfo.profile?.street || "", + number: userInfo.profile?.number || "", + complement: userInfo.profile?.complement || "", + neighborhood: userInfo.profile?.neighborhood || "", + city: userInfo.profile?.city || "", + state: userInfo.profile?.state || "", + }); + // Se já existe CEP, marcar como válido + if (userInfo.profile?.cep) { + setCepValid(true); + } + } + setIsEditing(!isEditing); + }; + + const handleSaveEdit = async () => { + try { + // Aqui você implementaria a chamada para atualizar o perfil + console.log('[PERFIL] Salvando alterações:', editingData); + // await atualizarPerfil(userInfo?.user.id, editingData); + setIsEditing(false); + setUserInfo((prev) => + prev ? { + ...prev, + profile: prev.profile ? { + ...prev.profile, + full_name: editingData.full_name || prev.profile.full_name, + phone: editingData.phone || prev.profile.phone, + avatar_url: editingData.avatar_url || prev.profile.avatar_url, + cep: editingData.cep || prev.profile.cep, + street: editingData.street || prev.profile.street, + number: editingData.number || prev.profile.number, + complement: editingData.complement || prev.profile.complement, + neighborhood: editingData.neighborhood || prev.profile.neighborhood, + city: editingData.city || prev.profile.city, + state: editingData.state || prev.profile.state, + } : null, + } : null + ); + } catch (err: any) { + console.error('[PERFIL] Erro ao salvar:', err); + } + }; + + const handleCancelEdit = () => { + setIsEditing(false); + setEditingData({}); + setCepValid(null); + }; + + const handleCepChange = async (cepValue: string) => { + // Formatar CEP + const formatted = formatCEP(cepValue); + setEditingData({...editingData, cep: formatted}); + + // Validar CEP + const isValid = validarCEP(cepValue); + setCepValid(isValid ? null : false); // null = não validado ainda, false = inválido + + if (isValid) { + setCepLoading(true); + try { + const resultado = await buscarCEP(cepValue); + if (resultado) { + setCepValid(true); + // Preencher campos automaticamente + setEditingData(prev => ({ + ...prev, + street: resultado.street, + neighborhood: resultado.neighborhood, + city: resultado.city, + state: resultado.state, + })); + console.log('[PERFIL] CEP preenchido com sucesso:', resultado); + } else { + setCepValid(false); + } + } catch (err) { + console.error('[PERFIL] Erro ao buscar CEP:', err); + setCepValid(false); + } finally { + setCepLoading(false); + } + } + }; + + const handlePhoneChange = (phoneValue: string) => { + const formatted = formatTelefone(phoneValue); + setEditingData({...editingData, phone: formatted}); + }; + + return ( +
+
+
+ {/* Header com Título e Botão */} +
+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
+ {!isEditing ? ( + + ) : ( +
+ + +
+ )} +
+ + {/* Grid de 2 colunas */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

+ +
+ {/* Nome Completo */} +
+ + {isEditing ? ( + setEditingData({...editingData, full_name: e.target.value})} + className="mt-2" + /> + ) : ( + <> +
+ {userInfo.profile?.full_name || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+ + )} +
+ + {/* Email */} +
+ +
+ {userInfo.user.email} +
+

+ Este campo não pode ser alterado +

+
+ + {/* UUID */} +
+ +
+ {userInfo.user.id} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Permissões */} +
+ +
+ {userInfo.roles && userInfo.roles.length > 0 ? ( + userInfo.roles.map((role) => ( + + {role} + + )) + ) : ( + + Nenhuma permissão atribuída + + )} +
+
+
+
+ + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Telefone */} +
+ + {isEditing ? ( + handlePhoneChange(e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + maxLength={15} + /> + ) : ( +
+ {userInfo.profile?.phone || "Não preenchido"} +
+ )} +
+ + {/* Endereço */} +
+ + {isEditing ? ( + setEditingData({...editingData, street: e.target.value})} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {userInfo.profile?.street || "Não preenchido"} +
+ )} +
+ + {/* Número */} +
+ + {isEditing ? ( + setEditingData({...editingData, number: e.target.value})} + className="mt-2" + placeholder="123" + /> + ) : ( +
+ {userInfo.profile?.number || "Não preenchido"} +
+ )} +
+ + {/* Complemento */} +
+ + {isEditing ? ( + setEditingData({...editingData, complement: e.target.value})} + className="mt-2" + placeholder="Apto 42, Bloco B, etc." + /> + ) : ( +
+ {userInfo.profile?.complement || "Não preenchido"} +
+ )} +
+ + {/* Bairro */} +
+ + {isEditing ? ( + setEditingData({...editingData, neighborhood: e.target.value})} + className="mt-2" + placeholder="Vila, bairro, etc." + /> + ) : ( +
+ {userInfo.profile?.neighborhood || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditing ? ( + setEditingData({...editingData, city: e.target.value})} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {userInfo.profile?.city || "Não preenchido"} +
+ )} +
+ + {/* Estado */} +
+ + {isEditing ? ( + setEditingData({...editingData, state: e.target.value})} + className="mt-2" + placeholder="SP" + maxLength={2} + /> + ) : ( +
+ {userInfo.profile?.state || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditing ? ( +
+
+
+ handleCepChange(e.target.value)} + className="mt-2" + placeholder="00000-000" + maxLength={9} + disabled={cepLoading} + /> +
+ {cepValid === true && ( + + )} + {cepValid === false && ( + + )} +
+ {cepLoading && ( +

Buscando CEP...

+ )} + {cepValid === false && ( +

CEP inválido ou não encontrado

+ )} + {cepValid === true && ( +

✓ CEP preenchido com sucesso

+ )} +
+ ) : ( +
+ {userInfo.profile?.cep || "Não preenchido"} +
+ )} +
+
+
+
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ + {isEditing ? ( +
+ setEditingData({...editingData, avatar_url: newUrl})} + userName={editingData.full_name || userInfo.profile?.full_name || "Usuário"} + /> +
+ ) : ( +
+ + + + {getInitials(userInfo.profile?.full_name)} + + + +
+

+ {getInitials(userInfo.profile?.full_name)} +

+
+
+ )} + + {/* Informações de Status */} +
+
+ +
+ + {userInfo.profile?.disabled ? "Desabilitado" : "Ativo"} + +
+
+
+
+
+
+ + {/* Botão Voltar */} +
+ +
+
+
+
+ ); +} diff --git a/susconecta/app/agenda/page.tsx b/susconecta/app/agenda/page.tsx index 747b7d1..5a7630d 100644 --- a/susconecta/app/agenda/page.tsx +++ b/susconecta/app/agenda/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; import HeaderAgenda from "@/components/agenda/HeaderAgenda"; import FooterAgenda from "@/components/agenda/FooterAgenda"; @@ -38,7 +38,6 @@ interface FormData { export default function NovoAgendamentoPage() { const router = useRouter(); - const searchParams = useSearchParams(); const [formData, setFormData] = useState({}); const handleFormChange = (data: FormData) => { @@ -88,7 +87,7 @@ export default function NovoAgendamentoPage() { const handleCancel = () => { // If origin was provided (eg: consultas), return there. Default to calendar. try { - const origin = searchParams?.get?.('origin'); + const origin = (typeof window !== 'undefined') ? new URLSearchParams(window.location.search).get('origin') : null; if (origin === 'consultas') { router.push('/consultas'); return; diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 9204e4f..47f4d95 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -17,8 +17,10 @@ import Link from 'next/link' import ProtectedRoute from '@/components/ProtectedRoute' import { useAuth } from '@/hooks/useAuth' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarMensagensPorPaciente } from '@/lib/api' -import { useReports } from '@/hooks/useReports' +import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, atualizarPaciente, buscarPacientePorId } from '@/lib/api' +import { ENV_CONFIG } from '@/lib/env-config' +import { listarRelatoriosPorPaciente } from '@/lib/reports' +// reports are rendered statically for now // Simulação de internacionalização básica const strings = { dashboard: 'Dashboard', @@ -53,7 +55,7 @@ const strings = { export default function PacientePage() { const { logout, user } = useAuth() - const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard') + const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'perfil'>('dashboard') // Simulação de loaders, empty states e erro const [loading, setLoading] = useState(false) @@ -236,44 +238,142 @@ export default function PacientePage() { const handleProfileChange = (field: string, value: string) => { setProfileData((prev: any) => ({ ...prev, [field]: value })) } - const handleSaveProfile = () => { - setIsEditingProfile(false) - setToast({ type: 'success', msg: strings.sucesso }) + const handleSaveProfile = async () => { + if (!patientId) { + setToast({ type: 'error', msg: 'Paciente não identificado. Não foi possível salvar.' }) + setIsEditingProfile(false) + return + } + setLoading(true) + try { + const payload: any = {} + if (profileData.email) payload.email = profileData.email + if (profileData.telefone) payload.phone_mobile = profileData.telefone + if (profileData.endereco) payload.street = profileData.endereco + if (profileData.cidade) payload.city = profileData.cidade + if (profileData.cep) payload.cep = profileData.cep + if (profileData.biografia) payload.notes = profileData.biografia + + await atualizarPaciente(String(patientId), payload) + + // refresh patient row + const refreshed = await buscarPacientePorId(String(patientId)).catch(() => null) + if (refreshed) { + const getFirst = (obj: any, keys: string[]) => { + if (!obj) return undefined + for (const k of keys) { + const v = obj[k] + if (v !== undefined && v !== null && String(v).trim() !== '') return String(v) + } + return undefined + } + const nome = getFirst(refreshed, ['full_name','fullName','name','nome','social_name']) || profileData.nome + const telefone = getFirst(refreshed, ['phone_mobile','phone','telefone','mobile']) || profileData.telefone + const rua = getFirst(refreshed, ['street','logradouro','endereco','address']) + const numero = getFirst(refreshed, ['number','numero']) + const bairro = getFirst(refreshed, ['neighborhood','bairro']) + const endereco = rua ? (numero ? `${rua}, ${numero}` : rua) + (bairro ? ` - ${bairro}` : '') : profileData.endereco + const cidade = getFirst(refreshed, ['city','cidade','localidade']) || profileData.cidade + const cep = getFirst(refreshed, ['cep','postal_code','zip']) || profileData.cep + const biografia = getFirst(refreshed, ['biography','bio','notes']) || profileData.biografia || '' + const emailFromRow = getFirst(refreshed, ['email']) || profileData.email + const foto = getFirst(refreshed, ['foto_url','avatar_url','fotoUrl']) || profileData.foto_url + setProfileData((prev:any) => ({ ...prev, nome, email: emailFromRow, telefone, endereco, cidade, cep, biografia, foto_url: foto })) + } + + setIsEditingProfile(false) + setToast({ type: 'success', msg: strings.sucesso }) + } catch (err: any) { + console.warn('[PacientePage] erro ao atualizar paciente', err) + setToast({ type: 'error', msg: err?.message || strings.erroSalvar }) + } finally { + setLoading(false) + } } const handleCancelEdit = () => { setIsEditingProfile(false) } function DashboardCards() { + const [nextAppt, setNextAppt] = useState(null) + const [examsCount, setExamsCount] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + let mounted = true + async function load() { + if (!patientId) { + setNextAppt(null) + setExamsCount(null) + return + } + setLoading(true) + try { + // Load appointments for this patient (upcoming) + const q = `patient_id=eq.${encodeURIComponent(String(patientId))}&order=scheduled_at.asc&limit=200` + const ags = await listarAgendamentos(q).catch(() => []) + if (!mounted) return + const now = Date.now() + // find the first appointment with scheduled_at >= now + const upcoming = (ags || []).map((a: any) => ({ ...a, _sched: a.scheduled_at ? new Date(a.scheduled_at).getTime() : null })) + .filter((a: any) => a._sched && a._sched >= now) + .sort((x: any, y: any) => Number(x._sched) - Number(y._sched)) + if (upcoming && upcoming.length) { + setNextAppt(new Date(upcoming[0]._sched).toLocaleDateString('pt-BR')) + } else { + setNextAppt(null) + } + + // Load reports/laudos count + const reports = await listarRelatoriosPorPaciente(String(patientId)).catch(() => []) + if (!mounted) return + setExamsCount(Array.isArray(reports) ? reports.length : 0) + } catch (e) { + console.warn('[DashboardCards] erro ao carregar dados', e) + if (!mounted) return + setNextAppt(null) + setExamsCount(null) + } finally { + if (mounted) setLoading(false) + } + } + load() + return () => { mounted = false } + }, [patientId]) + return ( -
+
{strings.proximaConsulta} - 12/10/2025 + {loading ? '...' : (nextAppt ?? '-')} {strings.ultimosExames} - 2 + {loading ? '...' : (examsCount !== null ? String(examsCount) : '-')} - - - {strings.mensagensNaoLidas} - 1 - -
+
) } // Consultas fictícias const [currentDate, setCurrentDate] = useState(new Date()) + + // helper: produce a local YYYY-MM-DD key (uses local timezone, not toISOString UTC) + const localDateKey = (d: Date) => { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` + } + const consultasFicticias = [ { id: 1, medico: "Dr. Carlos Andrade", especialidade: "Cardiologia", local: "Clínica Coração Feliz", - data: new Date().toISOString().split('T')[0], + data: localDateKey(new Date()), hora: "09:00", status: "Confirmada" }, @@ -282,7 +382,7 @@ export default function PacientePage() { medico: "Dra. Fernanda Lima", especialidade: "Dermatologia", local: "Clínica Pele Viva", - data: new Date().toISOString().split('T')[0], + data: localDateKey(new Date()), hora: "14:30", status: "Pendente" }, @@ -291,7 +391,7 @@ export default function PacientePage() { medico: "Dr. João Silva", especialidade: "Ortopedia", local: "Hospital Ortopédico", - data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(), + data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return localDateKey(d) })(), hora: "11:00", status: "Cancelada" }, @@ -310,7 +410,7 @@ export default function PacientePage() { setCurrentDate(new Date()); } - const todayStr = currentDate.toISOString().split('T')[0]; + const todayStr = localDateKey(currentDate) const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr); function Consultas() { @@ -323,19 +423,132 @@ export default function PacientePage() { const activeToggleClass = "w-full transition duration-200 focus-visible:ring-2 focus-visible:ring-[#2563eb]/60 active:scale-[0.97] bg-[#2563eb] text-white hover:bg-[#2563eb] hover:text-white" const inactiveToggleClass = "w-full transition duration-200 bg-slate-50 text-[#2563eb] border border-[#2563eb]/30 hover:bg-slate-100 hover:text-[#2563eb] dark:bg-white/5 dark:text-white dark:hover:bg-white/10 dark:border-white/20" const hoverPrimaryIconClass = "rounded-xl bg-white text-[#1e293b] border border-black/10 shadow-[0_2px_8px_rgba(0,0,0,0.03)] transition duration-200 hover:bg-[#2563eb] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563eb] dark:bg-slate-800 dark:text-slate-100 dark:border-white/10 dark:shadow-none dark:hover:bg-[#2563eb] dark:hover:text-white" - const today = new Date(); today.setHours(0, 0, 0, 0); - const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0); - const isSelectedDateToday = selectedDate.getTime() === today.getTime() + const today = new Date(); today.setHours(0, 0, 0, 0); + const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0); + const isSelectedDateToday = selectedDate.getTime() === today.getTime() - const handlePesquisar = () => { - const params = new URLSearchParams({ - tipo: tipoConsulta, - especialidade, - local: localizacao - }) - router.push(`/resultados?${params.toString()}`) + + + // Appointments state (loaded when "Ver consultas agendadas" is opened) + const [appointments, setAppointments] = useState(null) + const [loadingAppointments, setLoadingAppointments] = useState(false) + const [appointmentsError, setAppointmentsError] = useState(null) + + useEffect(() => { + let mounted = true + if (!mostrarAgendadas) return + if (!patientId) { + setAppointmentsError('Paciente não identificado. Faça login novamente.') + return + } + + async function loadAppointments() { + try { + setLoadingAppointments(true) + setAppointmentsError(null) + setAppointments(null) + + // Try `eq.` first, then fallback to `in.(id)` which some views expect + const baseEncoded = encodeURIComponent(String(patientId)) + const queriesToTry = [ + `patient_id=eq.${baseEncoded}&order=scheduled_at.asc&limit=200`, + `patient_id=in.(${baseEncoded})&order=scheduled_at.asc&limit=200`, + ]; + + let rows: any[] = [] + for (const q of queriesToTry) { + try { + // Debug: also fetch raw response to inspect headers/response body in the browser + try { + const token = (typeof window !== 'undefined') ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null + const headers: Record = { + apikey: ENV_CONFIG.SUPABASE_ANON_KEY, + Accept: 'application/json', + } + if (token) headers.Authorization = `Bearer ${token}` + const rawUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/appointments?${q}` + console.debug('[Consultas][debug] GET', rawUrl, 'Headers(masked):', { ...headers, Authorization: headers.Authorization ? `${String(headers.Authorization).slice(0,6)}...${String(headers.Authorization).slice(-6)}` : undefined }) + const rawRes = await fetch(rawUrl, { method: 'GET', headers }) + const rawText = await rawRes.clone().text().catch(() => '') + console.debug('[Consultas][debug] raw response', { url: rawUrl, status: rawRes.status, bodyPreview: (typeof rawText === 'string' && rawText.length > 0) ? rawText.slice(0, 200) : rawText }) + } catch (dbgErr) { + console.debug('[Consultas][debug] não foi possível capturar raw response', dbgErr) + } + + const r = await listarAgendamentos(q) + if (r && Array.isArray(r) && r.length) { + rows = r + break + } + // if r is empty array, continue to next query format + } catch (e) { + // keep trying next format + console.debug('[Consultas] tentativa listarAgendamentos falhou para query', q, e) + } + } + + if (!mounted) return + if (!rows || rows.length === 0) { + // no appointments found for this patient using either filter + setAppointments([]) + return + } + + const doctorIds = Array.from(new Set(rows.map((r: any) => r.doctor_id).filter(Boolean))) + const doctorsMap: Record = {} + if (doctorIds.length) { + try { + const docs = await buscarMedicosPorIds(doctorIds).catch(() => []) + for (const d of docs || []) doctorsMap[d.id] = d + } catch (e) { + // ignore + } + } + + const mapped = (rows || []).map((a: any) => { + const sched = a.scheduled_at ? new Date(a.scheduled_at) : null + const doc = a.doctor_id ? doctorsMap[String(a.doctor_id)] : null + return { + id: a.id, + medico: doc?.full_name || a.doctor_id || '---', + especialidade: doc?.specialty || '', + local: a.location || a.place || '', + data: sched ? localDateKey(sched) : '', + hora: sched ? sched.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '', + status: a.status ? String(a.status) : 'Pendente', + } + }) + + setAppointments(mapped) + } catch (err: any) { + console.warn('[Consultas] falha ao carregar agendamentos', err) + if (!mounted) return + setAppointmentsError(err?.message ?? 'Falha ao carregar agendamentos.') + setAppointments([]) + } finally { + if (mounted) setLoadingAppointments(false) + } + } + + loadAppointments() + return () => { mounted = false } + }, [mostrarAgendadas, patientId]) + + // Monta a URL de resultados com os filtros atuais + const buildResultadosHref = () => { + const qs = new URLSearchParams() + qs.set('tipo', tipoConsulta) // 'teleconsulta' | 'presencial' + if (especialidade) qs.set('especialidade', especialidade) + if (localizacao) qs.set('local', localizacao) + // indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect) + qs.set('origin', 'paciente') + return `/resultados?${qs.toString()}` } + // derived lists for the "Ver consultas agendadas" dialog (computed after appointments state is declared) + const _dialogSource = (appointments !== null ? appointments : consultasFicticias) + const _todaysAppointments = (_dialogSource || []).filter((c: any) => c.data === todayStr) + return (
@@ -398,11 +611,11 @@ export default function PacientePage() {
-
@@ -419,7 +632,7 @@ export default function PacientePage() {
setMostrarAgendadas(open)}> - + Consultas agendadas Gerencie suas consultas confirmadas, pendentes ou canceladas. @@ -431,7 +644,7 @@ export default function PacientePage() { type="button" variant="outline" size="icon" - onClick={() => navigateDate('prev')} + onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('prev') }} aria-label="Dia anterior" className={`group shadow-sm ${hoverPrimaryIconClass}`} > @@ -442,7 +655,7 @@ export default function PacientePage() { type="button" variant="outline" size="icon" - onClick={() => navigateDate('next')} + onClick={(e: any) => { e.stopPropagation(); e.preventDefault(); navigateDate('next') }} aria-label="Próximo dia" className={`group shadow-sm ${hoverPrimaryIconClass}`} > @@ -462,84 +675,95 @@ export default function PacientePage() { )}
- {consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''} + {`${_todaysAppointments.length} consulta${_todaysAppointments.length !== 1 ? 's' : ''} agendada${_todaysAppointments.length !== 1 ? 's' : ''}`}
-
- {consultasDoDia.length === 0 ? ( -
- -

Nenhuma consulta agendada para este dia

-

Use a busca para marcar uma nova consulta.

-
+
+ {loadingAppointments && mostrarAgendadas ? ( +
Carregando consultas...
+ ) : appointmentsError ? ( +
{appointmentsError}
) : ( - consultasDoDia.map(consulta => ( -
-
-
- -
-
- - {consulta.medico} + // prefer appointments (client-loaded) when present; fallback to fictitious list + (() => { + const todays = _todaysAppointments + if (!todays || todays.length === 0) { + return ( +
+ +

Nenhuma consulta agendada para este dia

+

Use a busca para marcar uma nova consulta.

+
+ ) + } + return todays.map((consulta: any) => ( +
+
+
+ +
+
+ + {consulta.medico} +
+

+ {consulta.especialidade} • {consulta.local} +

-

- {consulta.especialidade} • {consulta.local} -

-
-
- - {consulta.hora} -
+
+ + {consulta.hora} +
-
- - {consulta.status} - -
+
+ + {consulta.status} + +
-
- - {consulta.status !== 'Cancelada' && ( - - )} - {consulta.status !== 'Cancelada' && ( +
- )} + {consulta.status !== 'Cancelada' && ( + + )} + {consulta.status !== 'Cancelada' && ( + + )} +
-
- )) + )) + })() )}
- @@ -549,120 +773,164 @@ export default function PacientePage() { ) } - // Reports (laudos) hook - const { reports, loadReportsByPatient, loading: reportsLoading } = useReports() + // Selected report state const [selectedReport, setSelectedReport] = useState(null) function ExamesLaudos() { + const [reports, setReports] = useState(null) + const [loadingReports, setLoadingReports] = useState(false) + const [reportsError, setReportsError] = useState(null) + const [reportDoctorName, setReportDoctorName] = useState(null) + useEffect(() => { + let mounted = true if (!patientId) return - // load laudos for this patient - loadReportsByPatient(patientId).catch(() => {}) + setLoadingReports(true) + setReportsError(null) + listarRelatoriosPorPaciente(String(patientId)) + .then(res => { + if (!mounted) return + setReports(Array.isArray(res) ? res : []) + }) + .catch(err => { + console.warn('[ExamesLaudos] erro ao carregar laudos', err) + if (!mounted) return + setReportsError('Falha ao carregar laudos.') + }) + .finally(() => { if (mounted) setLoadingReports(false) }) + + return () => { mounted = false } }, [patientId]) + // When a report is selected, try to fetch doctor name if we have an id + useEffect(() => { + let mounted = true + if (!selectedReport) { + setReportDoctorName(null) + return + } + const maybeDoctorId = selectedReport.doctor_id || selectedReport.created_by || null + if (!maybeDoctorId) { + setReportDoctorName(null) + return + } + (async () => { + try { + const docs = await buscarMedicosPorIds([String(maybeDoctorId)]).catch(() => []) + if (!mounted) return + if (docs && docs.length) { + const doc0: any = docs[0] + setReportDoctorName(doc0.full_name || doc0.name || doc0.fullName || null) + } + } catch (e) { + // ignore + } + })() + return () => { mounted = false } + }, [selectedReport]) + return (

Laudos

- {reportsLoading ? ( -
Carregando laudos...
- ) : (!reports || reports.length === 0) ? ( -
Nenhum laudo salvo.
- ) : ( -
- {reports.map((r: any) => ( -
+
+ {loadingReports ? ( +
{strings.carregando}
+ ) : reportsError ? ( +
{reportsError}
+ ) : (!reports || reports.length === 0) ? ( +
Nenhum laudo encontrado para este paciente.
+ ) : ( + reports.map((r) => ( +
-
{r.title || r.report_type || r.exame || r.name || 'Laudo'}
-
Data: {new Date(r.report_date || r.data || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
+
{r.title || r.name || r.report_name || 'Laudo'}
+
Data: {new Date(r.report_date || r.created_at || Date.now()).toLocaleDateString('pt-BR')}
- - + +
- ))} -
- )} + )) + )} +
!open && setSelectedReport(null)}> - - Laudo Médico - - {selectedReport && ( - <> -
{selectedReport.title || selectedReport.report_type || selectedReport.exame || 'Laudo'}
-
Data: {new Date(selectedReport.report_date || selectedReport.data || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
-
{selectedReport.content || selectedReport.laudo || selectedReport.body || JSON.stringify(selectedReport, null, 2)}
- - )} -
-
- - - -
+ + Laudo Médico + + {selectedReport && ( + <> +
+
{selectedReport.title || selectedReport.name || 'Laudo'}
+
Data: {new Date(selectedReport.report_date || selectedReport.created_at || Date.now()).toLocaleDateString('pt-BR')}
+ {reportDoctorName &&
Profissional: {reportDoctorName}
} +
+ + {/* Standardized laudo sections: CID, Exame, Diagnóstico, Conclusão, Notas (prefer HTML when available) */} + {(() => { + const cid = selectedReport.cid ?? selectedReport.cid_code ?? selectedReport.cidCode ?? selectedReport.cie ?? '-' + const exam = selectedReport.exam ?? selectedReport.exame ?? selectedReport.especialidade ?? selectedReport.report_type ?? '-' + const diagnosis = selectedReport.diagnosis ?? selectedReport.diagnostico ?? selectedReport.diagnosis_text ?? selectedReport.diagnostico_text ?? '' + const conclusion = selectedReport.conclusion ?? selectedReport.conclusao ?? selectedReport.conclusion_text ?? selectedReport.conclusao_text ?? '' + const notesHtml = selectedReport.content_html ?? selectedReport.conteudo_html ?? selectedReport.contentHtml ?? null + const notesText = selectedReport.content ?? selectedReport.body ?? selectedReport.conteudo ?? selectedReport.notes ?? selectedReport.observacoes ?? '' + return ( +
+
+
CID
+
{cid || '-'}
+
+ +
+
Exame
+
{exam || '-'}
+
+ +
+
Diagnóstico
+
{diagnosis || '-'}
+
+ +
+
Conclusão
+
{conclusion || '-'}
+
+ +
+
Notas do Profissional
+ {notesHtml ? ( +
+ ) : ( +
{notesText || '-'}
+ )} +
+
+ ) + })()} + {/* Optional: doctor signature or footer */} + {selectedReport.doctor_signature && ( +
Assinatura: assinatura
+ )} + + )} + + + + + +
) } - function Mensagens() { - const [msgs, setMsgs] = useState([]) - const [loadingMsgs, setLoadingMsgs] = useState(false) - const [msgsError, setMsgsError] = useState(null) - - useEffect(() => { - let mounted = true - if (!patientId) return - setLoadingMsgs(true) - setMsgsError(null) - listarMensagensPorPaciente(String(patientId)) - .then(res => { - if (!mounted) return - setMsgs(Array.isArray(res) ? res : []) - }) - .catch(err => { - console.warn('[Mensagens] erro ao carregar mensagens', err) - if (!mounted) return - setMsgsError('Falha ao carregar mensagens.') - }) - .finally(() => { if (mounted) setLoadingMsgs(false) }) - - return () => { mounted = false } - }, [patientId]) - - return ( -
-

Mensagens Recebidas

-
- {loadingMsgs ? ( -
Carregando mensagens...
- ) : msgsError ? ( -
{msgsError}
- ) : (!msgs || msgs.length === 0) ? ( -
Nenhuma mensagem encontrada.
- ) : ( - msgs.map((msg: any) => ( -
-
- - {msg.sender_name || msg.from || msg.doctor_name || 'Remetente'} - {!msg.read && Nova} -
-
{new Date(msg.created_at || msg.data || Date.now()).toLocaleString('pt-BR')}
-
{msg.body || msg.content || msg.text || JSON.stringify(msg)}
-
- )) - )} -
-
- ) - } + function Perfil() { - const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep || profileData.biografia) + const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep) return (
@@ -732,14 +1000,7 @@ export default function PacientePage() {

{profileData.cep}

)}
-
- - {isEditingProfile ? ( -