- 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.
7.7 KiB
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
- ✅ Card Google Calendar exibe status badge: Verde "Conectado ✅" ou Cinza "Não conectado ❌"
- ✅ Se conectado, botão muda para "Reconectar" (permite trocar email OAuth)
- ✅ Status é carregado ao montar o componente (via Supabase ou lógica de verificação)
- ✅ Loading state exibido enquanto status carrega
- ✅ 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)