saveinmed/frontend/src/pages/dashboard/admin/OrdersPage.tsx
eycksilva 3559afc1f7 feat(ui): padronizar paleta #0F4C81 e estrutura em múltiplas telas
- SellerDashboard: migrado para Shell (header topo), removida sidebar lateral,
  cards KPI brancos com react-icons pretos (FaChartLine, FaBoxOpen, FaReceipt)
- Shell: adicionados todos os links de nav para owner/seller no header
  (Estoque, Buscar Produtos, Pedidos, Carteira, Equipe, Config. Entrega)
- Wallet: ícone FaMoneyCheck no botão Solicitar Saque, card saldo com #0F4C81,
  thead da tabela com #0F4C81, fix R$ NaN (formatCurrency null-safe)
- Team: botões e thead com #0F4C81, emojis removidos dos roleLabels
- ShippingSettings: wrapped com Shell (mantém header), emojis substituídos por
  react-icons pretos (FaTruck, FaLocationDot, FaStore, FaCircleInfo, FaFloppyDisk),
  botão Salvar com #0F4C81
- Orders: removido box cinza de fundo dos ícones nas abas e estado vazio
- LocationPicker: fallback seguro para OpenStreetMap quando VITE_MAP_TILE_LAYER
  não está definido (corrige tela branca em /search)
- Inventory/Cart: cores dos botões e thead atualizadas para #0F4C81
2026-02-26 15:56:03 -03:00

252 lines
12 KiB
TypeScript

import { useEffect, useState } from 'react'
import { adminService, Order, OrderItem } from '@/services/adminService'
const STATUS_OPTIONS = ['Pendente', 'Pago', 'Faturado', 'Entregue']
export function OrdersPage() {
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null)
const pageSize = 10
useEffect(() => {
loadOrders()
}, [page])
const loadOrders = async () => {
setLoading(true)
try {
const data = await adminService.listOrders(page, pageSize)
setOrders(data.orders || [])
setTotal(data.total)
} catch (err) {
console.error('Error loading orders:', err)
} finally {
setLoading(false)
}
}
const handleStatusChange = async (id: string, status: string) => {
try {
await adminService.updateOrderStatus(id, status)
loadOrders()
} catch (err) {
console.error('Error updating order status:', err)
alert('Erro ao atualizar status')
}
}
const handleDelete = async (id: string) => {
if (!confirm('Tem certeza que deseja excluir este pedido?')) return
try {
await adminService.deleteOrder(id)
loadOrders()
} catch (err) {
console.error('Error deleting order:', err)
alert('Erro ao excluir pedido')
}
}
const formatPrice = (cents: number) => {
return (cents / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getStatusColor = (status: string) => {
switch (status) {
case 'Pendente': return 'bg-yellow-100 text-yellow-800'
case 'Pago': return 'bg-blue-100 text-blue-800'
case 'Faturado': return 'bg-purple-100 text-purple-800'
case 'Entregue': return 'bg-green-100 text-green-800'
default: return 'bg-gray-100 text-gray-800'
}
}
const totalPages = Math.ceil(total / pageSize)
return (
<div>
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Pedidos</h1>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg bg-white shadow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="text-white" style={{ backgroundColor: '#0F4C81' }}>
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">ID</th>
<th className="px-4 py-3 text-left text-sm font-medium">Data</th>
<th className="px-4 py-3 text-left text-sm font-medium">Status</th>
<th className="px-4 py-3 text-right text-sm font-medium">Total</th>
<th className="px-4 py-3 text-right text-sm font-medium">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
Carregando...
</td>
</tr>
) : orders.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
Nenhum pedido encontrado
</td>
</tr>
) : (
orders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm font-mono text-gray-600">
{order.id.substring(0, 8)}...
</td>
<td className="px-4 py-3 text-sm text-gray-600">
{formatDate(order.created_at)}
</td>
<td className="px-4 py-3">
<select
value={order.status}
onChange={(e) => handleStatusChange(order.id, e.target.value)}
className={`rounded-full px-2 py-1 text-xs font-medium border-0 ${getStatusColor(order.status)}`}
>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
{formatPrice(order.total_cents)}
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => setSelectedOrder(order)}
className="mr-2 text-sm text-blue-600 hover:underline"
>
Ver
</button>
<button
onClick={() => handleDelete(order.id)}
className="text-sm text-red-600 hover:underline"
>
Excluir
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-gray-600">
Mostrando {(page - 1) * pageSize + 1} a {Math.min(page * pageSize, total)} de {total}
</p>
<div className="flex gap-2">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Próxima
</button>
</div>
</div>
)}
{/* Order Detail Modal */}
{selectedOrder && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg rounded-lg bg-white p-6 shadow-xl">
<h2 className="mb-4 text-xl font-bold">Detalhes do Pedido</h2>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500">ID:</span>
<p className="font-mono">{selectedOrder.id}</p>
</div>
<div>
<span className="text-gray-500">Status:</span>
<p className={`inline-block rounded-full px-2 py-1 text-xs font-medium ${getStatusColor(selectedOrder.status)}`}>
{selectedOrder.status}
</p>
</div>
<div>
<span className="text-gray-500">Comprador ID:</span>
<p className="font-mono text-xs">{selectedOrder.buyer_id}</p>
</div>
<div>
<span className="text-gray-500">Vendedor ID:</span>
<p className="font-mono text-xs">{selectedOrder.seller_id}</p>
</div>
</div>
{selectedOrder.items && selectedOrder.items.length > 0 && (
<div>
<h3 className="mb-2 font-medium text-gray-700">Itens</h3>
<div className="rounded border">
<table className="min-w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left">Produto</th>
<th className="px-3 py-2 text-right">Qtd</th>
<th className="px-3 py-2 text-right">Valor</th>
</tr>
</thead>
<tbody>
{selectedOrder.items.map((item, idx) => (
<tr key={idx} className="border-t">
<td className="px-3 py-2 font-mono text-xs">{item.product_id.substring(0, 8)}...</td>
<td className="px-3 py-2 text-right">{item.quantity}</td>
<td className="px-3 py-2 text-right">{formatPrice(item.unit_cents * item.quantity)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
<div className="border-t pt-4">
<div className="flex justify-between text-lg font-bold">
<span>Total:</span>
<span>{formatPrice(selectedOrder.total_cents)}</span>
</div>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setSelectedOrder(null)}
className="rounded border px-4 py-2 text-sm"
>
Fechar
</button>
</div>
</div>
</div>
)}
</div>
)
}