- 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.
366 lines
12 KiB
Markdown
366 lines
12 KiB
Markdown
# 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`:
|
|
|
|
```typescript
|
|
'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`:
|
|
|
|
```typescript
|
|
'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
|
|
|
|
- [x] Botão "Gerar QR Code" funciona em cards de instâncias desconectadas
|
|
- [x] Modal abre ao clicar no botão
|
|
- [x] Loading state exibido durante geração
|
|
- [x] QR code exibido corretamente (base64 ou URL)
|
|
- [x] Instruções de como escanear exibidas
|
|
- [x] Botão "Fechar" fecha o modal
|
|
- [x] Tecla ESC fecha o modal
|
|
- [x] Erro de API exibe mensagem apropriada
|
|
- [x] Modal é responsivo (mobile e desktop)
|
|
- [x] 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
|
|
- [x] Verificar estrutura de pastas atual do projeto
|
|
- [x] Criar componente QRCodeModal.tsx
|
|
- [x] Atualizar dashboard/page.tsx com funcionalidade de QR Code
|
|
- [x] Atualizar evolutionapi.ts para incluir campos qrcode e base64 na resposta
|
|
- [x] Corrigir erros de linting (adicionar type="button", aria-labels, title no SVG)
|
|
- [x] Executar validações (Biome check passou sem erros)
|
|
- [x] 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
|