- 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
252 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|