"use client"; import { Fragment, useEffect, useState } from 'react'; import { Menu, Listbox, Transition } from '@headlessui/react'; import CreatePlanModal from '@/components/plans/CreatePlanModal'; import EditPlanModal from '@/components/plans/EditPlanModal'; import ConfirmDialog from '@/components/layout/ConfirmDialog'; import Pagination from '@/components/layout/Pagination'; import { useToast } from '@/components/layout/ToastContext'; import { SparklesIcon, TrashIcon, PencilIcon, EllipsisVerticalIcon, MagnifyingGlassIcon, CheckIcon, ChevronUpDownIcon, PlusIcon, XMarkIcon } from '@heroicons/react/24/outline'; interface Plan { id: string; name: string; description?: string; monthly_price?: string; annual_price?: string; min_users: number; max_users: number; storage_gb: number; is_active: boolean; features?: string[]; differentiators?: string[]; solutions_count?: number; } const STATUS_OPTIONS = [ { id: 'all', name: 'Todos os Status' }, { id: 'active', name: 'Ativos' }, { id: 'inactive', name: 'Inativos' }, ]; export default function PlansPage() { const toast = useToast(); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [editingPlanId, setEditingPlanId] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const [planToDelete, setPlanToDelete] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; const [searchTerm, setSearchTerm] = useState(''); const [selectedStatus, setSelectedStatus] = useState(STATUS_OPTIONS[0]); useEffect(() => { fetchPlans(); }, []); const fetchPlans = async () => { try { const response = await fetch('/api/admin/plans', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (response.ok) { const data = await response.json(); const plansData = data.plans || []; // Buscar contagem de soluções para cada plano const plansWithSolutions = await Promise.all( plansData.map(async (plan: Plan) => { try { const solResponse = await fetch(`/api/admin/plans/${plan.id}/solutions`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (solResponse.ok) { const solData = await solResponse.json(); return { ...plan, solutions_count: solData.solutions?.length || 0 }; } } catch (e) { console.error('Erro ao buscar soluções do plano:', e); } return { ...plan, solutions_count: 0 }; }) ); setPlans(plansWithSolutions); } } catch (error) { console.error('Error fetching plans:', error); } finally { setLoading(false); } }; const handleDeleteClick = (id: string) => { setPlanToDelete(id); setConfirmOpen(true); }; const handleConfirmDelete = async () => { if (!planToDelete) return; try { const response = await fetch(`/api/admin/plans/${planToDelete}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (response.ok) { setPlans(plans.filter(p => p.id !== planToDelete)); selectedIds.delete(planToDelete); setSelectedIds(new Set(selectedIds)); toast.success('Plano excluído', 'O plano foi excluído com sucesso.'); } else { toast.error('Erro ao excluir', 'Não foi possível excluir o plano.'); } } catch (error) { console.error('Error deleting plan:', error); toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir o plano.'); } finally { setConfirmOpen(false); setPlanToDelete(null); } }; const handleDeleteSelected = () => { if (selectedIds.size === 0) return; setPlanToDelete('multiple'); setConfirmOpen(true); }; const handleConfirmDeleteMultiple = async () => { const idsToDelete = Array.from(selectedIds); let successCount = 0; for (const id of idsToDelete) { try { const response = await fetch(`/api/admin/plans/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); if (response.ok) successCount++; } catch (error) { console.error('Error deleting plan:', error); } } setPlans(plans.filter(p => !selectedIds.has(p.id))); setSelectedIds(new Set()); toast.success(`${successCount} plano(s) excluído(s)`, 'Os planos selecionados foram excluídos.'); setConfirmOpen(false); setPlanToDelete(null); }; const toggleActive = async (id: string, currentStatus: boolean) => { try { const response = await fetch(`/api/admin/plans/${id}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ is_active: !currentStatus }), }); if (response.ok) { setPlans(plans.map(p => p.id === id ? { ...p, is_active: !currentStatus } : p )); } } catch (error) { console.error('Error toggling plan status:', error); } }; const clearFilters = () => { setSearchTerm(''); setSelectedStatus(STATUS_OPTIONS[0]); }; const handleEdit = (planId: string) => { setEditingPlanId(planId); setIsEditModalOpen(true); }; const handleEditSuccess = () => { fetchPlans(); toast.success('Plano atualizado', 'O plano foi atualizado com sucesso.'); }; // Lógica de Filtragem const filteredPlans = plans.filter((plan) => { // Texto const searchLower = searchTerm.toLowerCase(); const matchesSearch = (plan.name?.toLowerCase() || '').includes(searchLower) || (plan.description?.toLowerCase() || '').includes(searchLower); // Status const matchesStatus = selectedStatus.id === 'all' ? true : selectedStatus.id === 'active' ? plan.is_active : !plan.is_active; return matchesSearch && matchesStatus; }); // Paginação const totalItems = filteredPlans.length; const totalPages = Math.ceil(totalItems / itemsPerPage); const paginatedPlans = filteredPlans.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage ); // Seleção múltipla const toggleSelectAll = () => { if (selectedIds.size === paginatedPlans.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(paginatedPlans.map(p => p.id))); } }; const toggleSelect = (id: string) => { const newSelected = new Set(selectedIds); if (newSelected.has(id)) { newSelected.delete(id); } else { newSelected.add(id); } setSelectedIds(newSelected); }; return (
{/* Header */}

Planos

Gerencie os planos de assinatura da plataforma.

{selectedIds.size > 0 && ( )}
{/* Toolbar de Filtros */}
{/* Busca */}
setSearchTerm(e.target.value)} />
{/* Filtro de Status */}
{selectedStatus.name} {STATUS_OPTIONS.map((status, statusIdx) => ( `relative cursor-default select-none py-2 pl-10 pr-4 ${active ? 'bg-zinc-100 dark:bg-zinc-700 text-zinc-900 dark:text-white' : 'text-zinc-900 dark:text-zinc-100' }` } value={status} > {({ selected }) => ( <> {status.name} {selected ? ( ) : null} )} ))}
{/* Botão Limpar */} {(searchTerm || selectedStatus.id !== 'all') && ( )}
{/* Tabela */} {loading ? (
) : filteredPlans.length === 0 ? (

Nenhum plano encontrado

Não encontramos resultados para os filtros selecionados. Tente limpar a busca ou alterar os filtros.

) : (
{paginatedPlans.map((plan) => ( handleEdit(plan.id)}> ))}
0} onChange={toggleSelectAll} className="w-4 h-4 text-[var(--brand-color)] bg-zinc-100 border-zinc-300 rounded focus:ring-[var(--brand-color)] dark:focus:ring-[var(--brand-color)] dark:ring-offset-zinc-900 focus:ring-2 dark:bg-zinc-700 dark:border-zinc-600" /> Plano Preços Usuários Armazenamento Soluções Status Ações
e.stopPropagation()}> toggleSelect(plan.id)} className="w-4 h-4 text-[var(--brand-color)] bg-zinc-100 border-zinc-300 rounded focus:ring-[var(--brand-color)] dark:focus:ring-[var(--brand-color)] dark:ring-offset-zinc-900 focus:ring-2 dark:bg-zinc-700 dark:border-zinc-600" />
{plan.name?.substring(0, 2).toUpperCase()}
{plan.name}
{plan.description && (
{plan.description}
)}
{plan.monthly_price ? `R$ ${plan.monthly_price}/mês` : '-'} {plan.annual_price ? `R$ ${plan.annual_price}/ano` : '-'}
{plan.min_users} - {plan.max_users} {plan.storage_gb} GB {plan.solutions_count || 0} {plan.solutions_count === 1 ? 'solução' : 'soluções'} e.stopPropagation()}> e.stopPropagation()}>
{({ active }) => ( )}
{({ active }) => ( )}
)} setIsCreateModalOpen(false)} onSuccess={(plan) => { setPlans([...plans, plan]); setIsCreateModalOpen(false); }} /> { setIsEditModalOpen(false); setEditingPlanId(null); }} planId={editingPlanId} onSuccess={handleEditSuccess} /> { setConfirmOpen(false); setPlanToDelete(null); }} onConfirm={planToDelete === 'multiple' ? handleConfirmDeleteMultiple : handleConfirmDelete} title={planToDelete === 'multiple' ? 'Excluir Planos' : 'Excluir Plano'} message={ planToDelete === 'multiple' ? `Tem certeza que deseja excluir ${selectedIds.size} plano(s)? Esta ação não pode ser desfeita.` : 'Tem certeza que deseja excluir este plano? Esta ação não pode ser desfeita.' } confirmText="Excluir" cancelText="Cancelar" variant="danger" />
); }