feat: adicionar notificações reais no painel admin, limpar no banco e habilitar exportar CSV nos pagamentos
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m38s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m38s
Details
This commit is contained in:
parent
39c06c03d7
commit
4bb1e4b690
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -27,12 +27,6 @@ const pages: Record<string, { title: string; subtitle: string }> = {
|
|||
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<any[]>([]);
|
||||
|
||||
const [adminName, setAdminName] = useState('Super Admin');
|
||||
const [adminEmail, setAdminEmail] = useState('admin@agendapro.com');
|
||||
|
|
@ -48,6 +42,18 @@ export default function AdminPage() {
|
|||
const notifRef = useRef<HTMLDivElement>(null);
|
||||
const profileRef = useRef<HTMLDivElement>(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() {
|
|||
</button>
|
||||
)}
|
||||
{notifications.length > 0 && (
|
||||
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px', color: 'var(--danger)' }} onClick={() => setNotifications([])}>
|
||||
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px', color: 'var(--danger)' }} onClick={clearAllNotifications}>
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando pagamentos...</div>;
|
||||
|
||||
return (
|
||||
|
|
@ -70,7 +93,7 @@ export default function Payments() {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button className="btn btn-secondary"><Download size={16} /> Exportar CSV</button>
|
||||
<button className="btn btn-secondary" onClick={handleExportCSV}><Download size={16} /> Exportar CSV</button>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 0 }}>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.`]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue