diff --git a/admin/src/app/api/notifications/route.ts b/admin/src/app/api/notifications/route.ts new file mode 100644 index 0000000..5645eb6 --- /dev/null +++ b/admin/src/app/api/notifications/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; + +export async function GET(req: NextRequest) { + try { + // Fetch admin notifications (where tenant_id is null) + const notifsRes = await query(` + SELECT + id, + type, + title, + message, + is_read as read, + created_at + FROM notifications + WHERE tenant_id IS NULL + ORDER BY created_at DESC + LIMIT 20 + `); + + // Format relative time helper + const formatted = notifsRes.rows.map(n => { + const diffMs = new Date().getTime() - new Date(n.created_at).getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + let timeStr = 'Agora mesmo'; + if (diffDays > 0) { + timeStr = `${diffDays} dia${diffDays > 1 ? 's' : ''} atrás`; + } else if (diffHours > 0) { + timeStr = `${diffHours} hora${diffHours > 1 ? 's' : ''} atrás`; + } else if (diffMins > 0) { + timeStr = `${diffMins} min${diffMins > 1 ? 's' : ''} atrás`; + } + + return { + id: n.id, + type: n.type, + title: n.title, + message: n.message, + read: n.read, + time: timeStr + }; + }); + + return NextResponse.json(formatted); + } catch (error: any) { + console.error('GET Admin Notifications error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PUT(req: NextRequest) { + try { + // Mark all admin notifications as read + await query('UPDATE notifications SET is_read = true WHERE tenant_id IS NULL'); + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error('PUT Admin Notifications error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + // Delete all admin notifications + await query('DELETE FROM notifications WHERE tenant_id IS NULL'); + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error('DELETE Admin Notifications error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/admin/src/app/page.tsx b/admin/src/app/page.tsx index 46f9910..fbd1c06 100644 --- a/admin/src/app/page.tsx +++ b/admin/src/app/page.tsx @@ -27,12 +27,6 @@ const pages: Record = { settings: { title: 'Configurações', subtitle: 'Configurações do sistema' }, }; -const mockNotifications = [ - { id: 1, title: 'Nova Assinatura', message: 'Barbearia do Zé contratou o plano Starter', type: 'success', time: '5 min atrás', read: false }, - { id: 2, title: 'Falha no Pagamento', message: 'Salão Glamour não conseguiu renovar a assinatura', type: 'warning', time: '1 hora atrás', read: true }, - { id: 3, title: 'Ticket de Suporte', message: 'Novo chamado aberto por Studio Bella Nails', type: 'info', time: '2 horas atrás', read: true }, -]; - export default function AdminPage() { const router = useRouter(); const [currentPage, setCurrentPage] = useState('dashboard'); @@ -40,7 +34,7 @@ export default function AdminPage() { const [showNotifications, setShowNotifications] = useState(false); const [showProfile, setShowProfile] = useState(false); - const [notifications, setNotifications] = useState(mockNotifications); + const [notifications, setNotifications] = useState([]); const [adminName, setAdminName] = useState('Super Admin'); const [adminEmail, setAdminEmail] = useState('admin@agendapro.com'); @@ -48,6 +42,18 @@ export default function AdminPage() { const notifRef = useRef(null); const profileRef = useRef(null); + const fetchNotifications = async () => { + try { + const res = await fetch('/api/notifications'); + if (res.ok) { + const data = await res.json(); + setNotifications(data); + } + } catch (err) { + console.error('Error fetching admin notifications:', err); + } + }; + useEffect(() => { // Auth Check const token = localStorage.getItem('agendapro_admin_token'); @@ -59,12 +65,19 @@ export default function AdminPage() { setIsLoading(false); } + fetchNotifications(); + + const interval = setInterval(fetchNotifications, 15000); // Auto-refresh every 15s + const handleClickOutside = (e: MouseEvent) => { if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false); if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false); }; document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + clearInterval(interval); + }; }, [router]); const playNotificationSound = () => { @@ -106,8 +119,22 @@ export default function AdminPage() { setShowNotifications(false); }; - const markAllAsRead = () => { + const markAllAsRead = async () => { setNotifications(notifications.map(n => ({ ...n, read: true }))); + try { + await fetch('/api/notifications', { method: 'PUT' }); + } catch (err) { + console.error(err); + } + }; + + const clearAllNotifications = async () => { + setNotifications([]); + try { + await fetch('/api/notifications', { method: 'DELETE' }); + } catch (err) { + console.error(err); + } }; const logout = () => { @@ -203,7 +230,7 @@ export default function AdminPage() { )} {notifications.length > 0 && ( - )} diff --git a/admin/src/components/Payments.tsx b/admin/src/components/Payments.tsx index 4eb574e..a359067 100644 --- a/admin/src/components/Payments.tsx +++ b/admin/src/components/Payments.tsx @@ -44,6 +44,29 @@ export default function Payments() { // Helper to get unique months for the filter const months = Array.from(new Set(payments.map(p => p.date.substring(0, 7)))).sort().reverse(); + const handleExportCSV = () => { + if (filtered.length === 0) return; + const headers = ['Fatura', 'Estabelecimento', 'Plano', 'Valor', 'Status', 'Metodo', 'Data']; + const rows = filtered.map(p => [ + p.invoiceId, + p.tenant, + p.plan, + p.amount.toFixed(2), + statusMap[p.status]?.label || p.status, + p.method, + new Date(p.date + 'T12:00:00').toLocaleDateString('pt-BR') + ]); + const csvContent = [headers.join(','), ...rows.map(e => e.map(val => `"${val}"`).join(','))].join('\n'); + const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `pagamentos_${new Date().toISOString().slice(0,10)}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + if (loading) return
Carregando pagamentos...
; return ( @@ -70,7 +93,7 @@ export default function Payments() { ))} - +
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 2fbb0ae..baa145e 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -44,6 +44,13 @@ export async function POST(req: Request) { const newUser = userRes.rows[0]; + // 3.5 Criar notificação para o Admin + await query( + `INSERT INTO notifications (tenant_id, type, title, message) + VALUES (NULL, 'success', 'Novo Assinante', $1)`, + [`O estabelecimento ${name} (${slug}) se cadastrou no plano Starter.`] + ); + // 4. Gerar Token JWT const secret = process.env.JWT_SECRET || 'fallback_secret_for_development'; const token = jwt.sign( diff --git a/src/app/api/webhooks/asaas/route.ts b/src/app/api/webhooks/asaas/route.ts index 72a49f0..822d119 100644 --- a/src/app/api/webhooks/asaas/route.ts +++ b/src/app/api/webhooks/asaas/route.ts @@ -193,6 +193,15 @@ export async function POST(req: NextRequest) { WHERE id = $2`, [periodInterval, subscription.tenant_id] ); + + // Retrieve tenant name for notification + const tenantNameRes = await client.query('SELECT name FROM tenants WHERE id = $1', [subscription.tenant_id]); + const tenantName = tenantNameRes.rows[0]?.name || 'Estabelecimento'; + await client.query( + `INSERT INTO notifications (tenant_id, type, title, message) + VALUES (NULL, 'success', 'Pagamento Recebido', $1)`, + [`A assinatura de ${tenantName} foi paga no valor de R$ ${value}.`] + ); } } } @@ -219,6 +228,15 @@ export async function POST(req: NextRequest) { `UPDATE tenants SET subscription_status = 'cancelled', updated_at = NOW() WHERE id = $1`, [tenantId] ); + + // Retrieve tenant name for notification + const tenantNameRes = await client.query('SELECT name FROM tenants WHERE id = $1', [tenantId]); + const tenantName = tenantNameRes.rows[0]?.name || 'Estabelecimento'; + await client.query( + `INSERT INTO notifications (tenant_id, type, title, message) + VALUES (NULL, 'warning', 'Assinatura Cancelada', $1)`, + [`A assinatura de ${tenantName} foi cancelada.`] + ); } } }