Dashboard-Automatizase/docs/stories/story-3-3-exibir-status-calendar.md
Luis Erlacher 0152a2fda0 feat: add n8n API testing script for Google OAuth2 schema and existing credentials
- Implemented a bash script to test n8n API and retrieve credential schemas.
- Added types for API responses, Google Calendar, and WhatsApp instances.
- Configured Vitest for testing with React and added setup for testing-library.
2025-10-10 14:29:02 -03:00

7.7 KiB

Story 3.3: Exibir Status de Conexão do Google Calendar

Story Metadata

  • Epic: Epic 3 - Google Calendar OAuth Integration
  • Story ID: 3.3
  • Priority: P1 (High)
  • Effort Estimate: 1-2 hours
  • Status: Ready for Review
  • Assignee: TBD

User Story

Como usuário, Eu quero ver se meu Google Calendar está conectado ou não, Para que eu saiba o status atual da integração.

Acceptance Criteria

  1. Card Google Calendar exibe status badge: Verde "Conectado " ou Cinza "Não conectado "
  2. Se conectado, botão muda para "Reconectar" (permite trocar email OAuth)
  3. Status é carregado ao montar o componente (via Supabase ou lógica de verificação)
  4. Loading state exibido enquanto status carrega
  5. Usuário pode clicar "Reconectar" para re-autorizar com email diferente (conforme FR11)

Technical Implementation Notes

Update Dashboard to Load and Display Status

Atualizar /app/dashboard/page.tsx:

'use client'

import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import { getAllInstancesStatus, generateQRCode, disconnectInstance, type InstanceStatus } from '@/lib/evolutionapi'
import WhatsAppInstanceCard from '@/components/WhatsAppInstanceCard'
import GoogleCalendarCard from '@/components/GoogleCalendarCard'
import QRCodeModal from '@/components/QRCodeModal'
import ConfirmModal from '@/components/ConfirmModal'
import Toast from '@/components/Toast'

export default function DashboardPage() {
  const [userName, setUserName] = useState<string>('Usuário')
  const [instances, setInstances] = useState<InstanceStatus[]>([])
  const [loading, setLoading] = useState(true)

  // Google Calendar State
  const [calendarConnected, setCalendarConnected] = useState(false)
  const [calendarEmail, setCalendarEmail] = useState<string | null>(null)
  const [calendarLoading, setCalendarLoading] = useState(true)

  // ... (outros states existentes)

  useEffect(() => {
    const getUser = async () => {
      const { data: { user } } = await supabase.auth.getUser()
      if (user?.email) {
        const name = user.email.split('@')[0]
        setUserName(name)
      }
    }

    const init = async () => {
      await getUser()
      await Promise.all([
        loadInstances(),
        loadGoogleCalendarStatus()
      ])
    }

    init()

    // Check for OAuth callback
    const urlParams = new URLSearchParams(window.location.search)
    const oauthSuccess = urlParams.get('oauth_success')
    const oauthError = urlParams.get('oauth_error')

    if (oauthSuccess === 'true') {
      setToastMessage('Google Calendar conectado com sucesso!')
      setToastType('success')
      setToastVisible(true)
      window.history.replaceState({}, '', '/dashboard')
      loadGoogleCalendarStatus() // Reload status
    } else if (oauthError) {
      setToastMessage('Erro ao conectar Google Calendar. Tente novamente.')
      setToastType('error')
      setToastVisible(true)
      window.history.replaceState({}, '', '/dashboard')
    }
  }, [])

  const loadGoogleCalendarStatus = async () => {
    setCalendarLoading(true)
    try {
      const { data: { user } } = await supabase.auth.getUser()
      if (user) {
        const { data, error } = await supabase
          .from('portal.integrations')
          .select('status, connected_at')
          .eq('user_id', user.id)
          .eq('provider', 'google_calendar')
          .maybeSingle()

        if (error && error.code !== 'PGRST116') { // PGRST116 = no rows returned
          throw error
        }

        if (data && data.status === 'connected') {
          setCalendarConnected(true)
          // Optionally fetch email from Google API or store it in Supabase
          // For now, we can leave it null or fetch from user metadata
          setCalendarEmail(null) // TODO: implement email fetching if needed
        } else {
          setCalendarConnected(false)
          setCalendarEmail(null)
        }
      }
    } catch (error) {
      console.error('Error loading Google Calendar status:', error)
      setCalendarConnected(false)
    } finally {
      setCalendarLoading(false)
    }
  }

  const handleConnectGoogleCalendar = async () => {
    const { data: { user } } = await supabase.auth.getUser()
    if (!user) {
      setToastMessage('Erro: Usuário não autenticado')
      setToastType('error')
      setToastVisible(true)
      return
    }

    const n8nOAuthUrl = process.env.NEXT_PUBLIC_N8N_OAUTH_URL || process.env.N8N_OAUTH_URL
    if (!n8nOAuthUrl) {
      console.error('N8N_OAUTH_URL not configured')
      setToastMessage('Erro de configuração. Contate o administrador.')
      setToastType('error')
      setToastVisible(true)
      return
    }

    // Redirect to n8n OAuth flow
    const redirectUrl = `${n8nOAuthUrl}?user_id=${user.id}&redirect_to=${encodeURIComponent(window.location.origin + '/dashboard')}`
    window.location.href = redirectUrl
  }

  // ... (resto do código)

  return (
    <>
      <div className="space-y-6">
        {/* Welcome Section */}
        <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
          <h2 className="text-2xl font-bold text-white mb-2">
            Bem-vindo ao Portal AutomatizaSE, {userName}!
          </h2>
          <p className="text-gray-400">
            Gerencie suas integrações de WhatsApp e Google Calendar.
          </p>
        </div>

        {/* WhatsApp Instances Section */}
        <div>
          <h3 className="text-xl font-semibold text-white mb-4">Instâncias WhatsApp</h3>
          {/* ... (código existente) */}
        </div>

        {/* Google Calendar Section */}
        <div>
          <h3 className="text-xl font-semibold text-white mb-4">Integrações</h3>
          {calendarLoading ? (
            <div className="bg-gray-800 border border-gray-700 rounded-lg p-6">
              <div className="text-center py-4 text-gray-400">
                Carregando status do Google Calendar...
              </div>
            </div>
          ) : (
            <GoogleCalendarCard
              isConnected={calendarConnected}
              connectedEmail={calendarEmail}
              onConnect={handleConnectGoogleCalendar}
            />
          )}
        </div>
      </div>

      {/* Modals and Toasts */}
      {/* ... (código existente) */}
    </>
  )
}

Optional: Store Connected Email

Se quiser armazenar e exibir o email conectado, adicionar coluna na tabela:

ALTER TABLE portal.integrations ADD COLUMN connected_email VARCHAR(255);

E atualizar n8n para salvar o email também:

INSERT INTO portal.integrations (user_id, provider, status, connected_email, connected_at)
VALUES (:user_id, 'google_calendar', 'connected', :google_email, NOW())
ON CONFLICT (user_id, provider)
DO UPDATE SET status = 'connected', connected_email = :google_email, connected_at = NOW(), updated_at = NOW();

Testing Checklist

  • Status "Conectado" exibido quando usuário tem OAuth autorizado
  • Status "Não conectado" exibido quando usuário não tem OAuth
  • Badge verde/cinza exibe corretamente
  • Botão muda de "Conectar" para "Reconectar" quando conectado
  • Loading state exibido durante carregamento inicial
  • Clicar "Reconectar" permite trocar de email OAuth
  • Email conectado exibido (se implementado)
  • Status atualiza após callback OAuth bem-sucedido

Dependencies

  • Blocks: None (Epic 3 completo após esta story)
  • Blocked By: Story 3.1, 3.2

Notes

  • Email conectado é opcional - pode ser implementado futuramente
  • Status é verificado a cada load do dashboard
  • Usuário pode reconectar quantas vezes quiser (conforme FR11)
  • Considerar adicionar função no n8n para validar se token ainda é válido (futuro)