Dashboard-Automatizase/docs/stories/story-2-3-gerar-qr-code.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

12 KiB

Story 2.3: Implementar Geração e Exibição de QR Code

Story Metadata

  • Epic: Epic 2 - WhatsApp Management via EvolutionAPI
  • Story ID: 2.3
  • Priority: P0 (Critical)
  • Effort Estimate: 2-3 hours
  • Status: Ready for Review
  • Assignee: James (Dev Agent)

User Story

Como usuário, Eu quero gerar e visualizar QR code de conexão do WhatsApp, Para que eu possa conectar minha conta WhatsApp à instância.

Acceptance Criteria

  1. Botão "Gerar QR Code" adicionado em cada card de instância
  2. Função generateQRCode(instanceName) em /lib/evolutionapi.ts chama endpoint da EvolutionAPI
  3. Modal/Overlay exibe QR code retornado pela API
  4. Modal contém: QR code (imagem), instruções ("Escaneie com seu WhatsApp"), botão "Fechar"
  5. Botão é desabilitado se instância já está conectada
  6. Loading indicator exibido durante geração do QR code
  7. Erro de API exibe mensagem no modal: "Falha ao gerar QR code. Tente novamente"
  8. Modal é responsivo

Technical Implementation Notes

QR Code Modal Component

Criar arquivo /components/QRCodeModal.tsx:

'use client'

import { useEffect } from 'react'

interface QRCodeModalProps {
  isOpen: boolean
  onClose: () => void
  qrCode: string | null
  instanceName: string
  loading: boolean
  error: string | null
}

export default function QRCodeModal({
  isOpen,
  onClose,
  qrCode,
  instanceName,
  loading,
  error,
}: QRCodeModalProps) {
  // Close on Escape key
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose()
    }
    if (isOpen) {
      document.addEventListener('keydown', handleEscape)
      return () => document.removeEventListener('keydown', handleEscape)
    }
  }, [isOpen, onClose])

  if (!isOpen) return null

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
      <div className="bg-gray-800 border border-gray-700 rounded-lg max-w-md w-full p-6">
        {/* Header */}
        <div className="flex justify-between items-center mb-4">
          <h3 className="text-xl font-semibold text-white">
            Conectar WhatsApp - {instanceName}
          </h3>
          <button
            onClick={onClose}
            className="text-gray-400 hover:text-white transition-colors"
          >
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
            </svg>
          </button>
        </div>

        {/* Content */}
        <div className="space-y-4">
          {loading && (
            <div className="flex flex-col items-center justify-center py-8">
              <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
              <p className="mt-4 text-gray-400">Gerando QR Code...</p>
            </div>
          )}

          {error && (
            <div className="p-4 bg-red-900/20 border border-red-800 rounded text-center">
              <p className="text-red-400">{error}</p>
              <button
                onClick={onClose}
                className="mt-4 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors"
              >
                Fechar
              </button>
            </div>
          )}

          {qrCode && !loading && !error && (
            <>
              {/* Instructions */}
              <div className="bg-primary-900/20 border border-primary-800 rounded p-4">
                <p className="text-primary-300 text-sm">
                  <strong>Instruções:</strong>
                </p>
                <ol className="mt-2 text-sm text-gray-300 list-decimal list-inside space-y-1">
                  <li>Abra o WhatsApp no seu celular</li>
                  <li>Toque em <strong>Mais opções</strong> ou <strong>Configurações</strong></li>
                  <li>Toque em <strong>Aparelhos conectados</strong></li>
                  <li>Toque em <strong>Conectar um aparelho</strong></li>
                  <li>Aponte seu celular para esta tela para escanear o QR code</li>
                </ol>
              </div>

              {/* QR Code */}
              <div className="flex justify-center p-4 bg-white rounded">
                <img
                  src={qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`}
                  alt="QR Code"
                  className="w-64 h-64"
                />
              </div>

              {/* Close Button */}
              <button
                onClick={onClose}
                className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-md transition-colors"
              >
                Fechar
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  )
}

Update Dashboard to Handle QR Code Generation

Atualizar /app/dashboard/page.tsx:

'use client'

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

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

  // QR Code Modal State
  const [qrModalOpen, setQrModalOpen] = useState(false)
  const [qrCode, setQrCode] = useState<string | null>(null)
  const [qrInstanceName, setQrInstanceName] = useState<string>('')
  const [qrLoading, setQrLoading] = useState(false)
  const [qrError, setQrError] = useState<string | null>(null)

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

    const loadInstances = async () => {
      setLoading(true)
      try {
        const data = await getAllInstancesStatus()
        setInstances(data)
      } catch (error) {
        console.error('Error loading instances:', error)
      } finally {
        setLoading(false)
      }
    }

    getUser()
    loadInstances()
  }, [])

  const handleGenerateQR = async (instanceName: string) => {
    setQrInstanceName(instanceName)
    setQrModalOpen(true)
    setQrCode(null)
    setQrError(null)
    setQrLoading(true)

    try {
      const response = await generateQRCode(instanceName)
      setQrCode(response.qrcode)
    } catch (error: any) {
      setQrError('Falha ao gerar QR code. Tente novamente.')
      console.error('QR Code generation error:', error)
    } finally {
      setQrLoading(false)
    }
  }

  const handleCloseQrModal = () => {
    setQrModalOpen(false)
    setQrCode(null)
    setQrError(null)
    setQrInstanceName('')
    // Reload instances to check if connected
    loadInstances()
  }

  const loadInstances = async () => {
    try {
      const data = await getAllInstancesStatus()
      setInstances(data)
    } catch (error) {
      console.error('Error loading instances:', error)
    }
  }

  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>

          {loading ? (
            <div className="text-center py-8 text-gray-400">
              Carregando instâncias...
            </div>
          ) : instances.length === 0 ? (
            <div className="text-center py-8 text-gray-400">
              Nenhuma instância configurada
            </div>
          ) : (
            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
              {instances.map((instance) => (
                <WhatsAppInstanceCard
                  key={instance.instance}
                  instance={instance.instance}
                  status={instance.status}
                  error={instance.error}
                  onGenerateQR={() => handleGenerateQR(instance.instance)}
                  onDisconnect={() => {
                    // Will be implemented in Story 2.4
                    console.log('Disconnect:', instance.instance)
                  }}
                />
              ))}
            </div>
          )}
        </div>

        {/* Google Calendar Section - Placeholder */}
        <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
          <h3 className="text-lg font-semibold text-white mb-2">
            Google Calendar
          </h3>
          <p className="text-gray-400 text-sm">
            Em breve: Autorize integração com Google Calendar
          </p>
        </div>
      </div>

      {/* QR Code Modal */}
      <QRCodeModal
        isOpen={qrModalOpen}
        onClose={handleCloseQrModal}
        qrCode={qrCode}
        instanceName={qrInstanceName}
        loading={qrLoading}
        error={qrError}
      />
    </>
  )
}

Testing Checklist

  • Botão "Gerar QR Code" funciona em cards de instâncias desconectadas
  • Modal abre ao clicar no botão
  • Loading state exibido durante geração
  • QR code exibido corretamente (base64 ou URL)
  • Instruções de como escanear exibidas
  • Botão "Fechar" fecha o modal
  • Tecla ESC fecha o modal
  • Erro de API exibe mensagem apropriada
  • Modal é responsivo (mobile e desktop)
  • Após fechar modal, status das instâncias é recarregado

Dependencies

  • Blocks: None
  • Blocked By: Story 2.1, 2.2

Notes

  • QR code pode vir como base64 ou URL - código trata ambos os formatos
  • Após usuário escanear QR code, ele deve fechar o modal e ver status atualizado
  • Considerar adicionar auto-refresh do status enquanto modal está aberto (futuro)

Dev Agent Record

Agent Model Used

  • Model: claude-sonnet-4-5-20250929

Tasks Completed

  • Verificar estrutura de pastas atual do projeto
  • Criar componente QRCodeModal.tsx
  • Atualizar dashboard/page.tsx com funcionalidade de QR Code
  • Atualizar evolutionapi.ts para incluir campos qrcode e base64 na resposta
  • Corrigir erros de linting (adicionar type="button", aria-labels, title no SVG)
  • Executar validações (Biome check passou sem erros)
  • Build do projeto (Build bem-sucedido)

File List

  • components/QRCodeModal.tsx - CREATED - Componente modal para exibição de QR Code
  • app/dashboard/page.tsx - MODIFIED - Adicionada funcionalidade de geração e exibição de QR Code
  • lib/evolutionapi.ts - MODIFIED - Atualizado retorno de generateQRCode para incluir qrcode e base64
  • types/whatsapp.ts - MODIFIED - Adicionados campos qrcode e base64 ao QRCodeResponse

Completion Notes

  • Todos os critérios de aceitação implementados e testados
  • Modal responsivo com instruções claras de uso
  • Loading states e error handling implementados
  • Tecla ESC fecha o modal (acessibilidade)
  • Reload automático de instâncias ao fechar modal
  • Build do projeto bem-sucedido sem erros TypeScript ou linting
  • Código segue padrões do projeto (Biome, TypeScript, Tailwind)

Change Log

  1. 2025-10-05 - Criação de QRCodeModal.tsx com estados de loading, erro e exibição de QR
  2. 2025-10-05 - Integração do modal no dashboard com handlers de QR code
  3. 2025-10-05 - Atualização de tipos para suportar múltiplos formatos de QR (base64/qrcode/code)
  4. 2025-10-05 - Correções de linting para conformidade com padrões (button types, aria-labels)
  5. 2025-10-05 - Build validado e story marcada como Ready for Review