feat: redesign Orders page with tabs for purchases/sales
- Add tabs: 'Pedidos Feitos' (compras) and 'Pedidos Recebidos' (vendas) - Add stats bar with totals and pending count - Add progress tracker for purchase orders - Improved UI with icons and better styling - Actions only visible on sales tab
This commit is contained in:
parent
e39ed59264
commit
8a5ec57e9c
1 changed files with 230 additions and 69 deletions
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||
import { Shell } from '../layouts/Shell'
|
||||
import { apiClient } from '../services/apiClient'
|
||||
import { formatCents } from '../utils/format'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
interface Order {
|
||||
id: string
|
||||
|
|
@ -13,25 +14,39 @@ interface Order {
|
|||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
Pendente: 'bg-yellow-100 text-yellow-800',
|
||||
Pago: 'bg-blue-100 text-blue-800',
|
||||
Faturado: 'bg-purple-100 text-purple-800',
|
||||
Entregue: 'bg-green-100 text-green-800'
|
||||
Pendente: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||
Pago: 'bg-blue-100 text-blue-800 border-blue-300',
|
||||
Faturado: 'bg-purple-100 text-purple-800 border-purple-300',
|
||||
Entregue: 'bg-green-100 text-green-800 border-green-300'
|
||||
}
|
||||
|
||||
const statusIcons: Record<string, string> = {
|
||||
Pendente: '⏳',
|
||||
Pago: '💳',
|
||||
Faturado: '📄',
|
||||
Entregue: '✅'
|
||||
}
|
||||
|
||||
type TabType = 'compras' | 'vendas'
|
||||
|
||||
export function OrdersPage() {
|
||||
const { user } = useAuth()
|
||||
const [activeTab, setActiveTab] = useState<TabType>('compras')
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadOrders()
|
||||
}, [])
|
||||
}, [activeTab])
|
||||
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await apiClient.get<{ orders: Order[]; total: number }>('/v1/orders')
|
||||
const endpoint = activeTab === 'compras'
|
||||
? '/v1/orders?role=buyer'
|
||||
: '/v1/orders?role=seller'
|
||||
const response = await apiClient.get<{ orders: Order[]; total: number }>(endpoint)
|
||||
setOrders(response?.orders || [])
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
|
|
@ -53,85 +68,231 @@ export function OrdersPage() {
|
|||
|
||||
const formatCurrency = (cents: number | undefined | null) => formatCents(cents)
|
||||
|
||||
// Calculate stats
|
||||
const pendingCount = orders.filter(o => o.status === 'Pendente').length
|
||||
const totalValue = orders.reduce((sum, o) => sum + (o.total_cents || 0), 0)
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-medicalBlue">Pedidos</h1>
|
||||
<p className="text-sm text-gray-600">Gerenciamento de pedidos e status</p>
|
||||
<h1 className="text-2xl font-bold text-gray-800">Gestão de Pedidos</h1>
|
||||
<p className="text-gray-500">Acompanhe suas compras e vendas</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadOrders}
|
||||
className="rounded bg-medicalBlue px-4 py-2 text-sm font-semibold text-white hover:opacity-90"
|
||||
className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Atualizar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-medicalBlue"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded bg-red-100 p-4 text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{!loading && orders.length === 0 && (
|
||||
<div className="rounded bg-gray-100 p-8 text-center text-gray-600">
|
||||
Nenhum pedido encontrado
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{orders.map((order) => (
|
||||
<div key={order.id} className="rounded-lg bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">Pedido #{order.id.slice(0, 8)}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(order.created_at).toLocaleDateString('pt-BR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-medicalBlue">
|
||||
{formatCurrency(order.total_cents)}
|
||||
</p>
|
||||
<span className={`inline-block rounded px-2 py-1 text-xs font-semibold ${statusColors[order.status] || 'bg-gray-100'}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
{/* Tabs */}
|
||||
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab('compras')}
|
||||
className={`flex-1 py-4 px-6 text-center font-medium transition-all relative ${activeTab === 'compras'
|
||||
? 'text-blue-600 bg-blue-50'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-2xl">🛒</span>
|
||||
<div className="text-left">
|
||||
<p className="font-semibold">Pedidos Feitos</p>
|
||||
<p className="text-xs text-gray-400">Suas compras de outras farmácias</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
{order.status === 'Pendente' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Pago')}
|
||||
className="rounded bg-blue-500 px-3 py-1 text-xs text-white"
|
||||
>
|
||||
Marcar como Pago
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'Pago' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Faturado')}
|
||||
className="rounded bg-purple-500 px-3 py-1 text-xs text-white"
|
||||
>
|
||||
Faturar
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'Faturado' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Entregue')}
|
||||
className="rounded bg-green-500 px-3 py-1 text-xs text-white"
|
||||
>
|
||||
Marcar Entregue
|
||||
</button>
|
||||
)}
|
||||
{activeTab === 'compras' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('vendas')}
|
||||
className={`flex-1 py-4 px-6 text-center font-medium transition-all relative ${activeTab === 'vendas'
|
||||
? 'text-green-600 bg-green-50'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-2xl">💰</span>
|
||||
<div className="text-left">
|
||||
<p className="font-semibold">Pedidos Recebidos</p>
|
||||
<p className="text-xs text-gray-400">Vendas para outras farmácias</p>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === 'vendas' && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="flex items-center justify-between px-6 py-3 bg-gray-50 border-b border-gray-100">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">Total:</span>
|
||||
<span className="font-bold text-gray-800">{orders.length} pedidos</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">Valor:</span>
|
||||
<span className="font-bold text-blue-600">{formatCurrency(totalValue)}</span>
|
||||
</div>
|
||||
{pendingCount > 0 && (
|
||||
<div className="flex items-center gap-2 bg-yellow-100 px-3 py-1 rounded-full">
|
||||
<span className="text-sm text-yellow-700">{pendingCount} pendentes</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{loading && (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-red-700 flex items-center gap-3">
|
||||
<span className="text-xl">⚠️</span>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && orders.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<span className="text-5xl block mb-4">
|
||||
{activeTab === 'compras' ? '🛒' : '💰'}
|
||||
</span>
|
||||
<h3 className="text-lg font-medium text-gray-700">
|
||||
Nenhum pedido {activeTab === 'compras' ? 'feito' : 'recebido'} ainda
|
||||
</h3>
|
||||
<p className="text-gray-500 mt-1">
|
||||
{activeTab === 'compras'
|
||||
? 'Pesquise produtos e comece a comprar!'
|
||||
: 'Cadastre produtos e aguarde as vendas!'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Orders List */}
|
||||
<div className="space-y-4">
|
||||
{orders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="rounded-xl border border-gray-200 bg-white p-5 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-12 h-12 rounded-lg flex items-center justify-center text-2xl ${activeTab === 'compras' ? 'bg-blue-100' : 'bg-green-100'
|
||||
}`}>
|
||||
{statusIcons[order.status] || '📦'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">
|
||||
Pedido #{order.id.slice(0, 8).toUpperCase()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-0.5">
|
||||
{new Date(order.created_at).toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xl font-bold text-gray-800">
|
||||
{formatCurrency(order.total_cents)}
|
||||
</p>
|
||||
<span className={`inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold border ${statusColors[order.status] || 'bg-gray-100'}`}>
|
||||
{statusIcons[order.status]} {order.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions - only for sales */}
|
||||
{activeTab === 'vendas' && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex gap-2">
|
||||
{order.status === 'Pendente' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Pago')}
|
||||
className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
💳 Confirmar Pagamento
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'Pago' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Faturado')}
|
||||
className="flex items-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
📄 Faturar Pedido
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'Faturado' && (
|
||||
<button
|
||||
onClick={() => updateStatus(order.id, 'Entregue')}
|
||||
className="flex items-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 transition-colors"
|
||||
>
|
||||
✅ Confirmar Entrega
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'Entregue' && (
|
||||
<span className="text-green-600 text-sm font-medium flex items-center gap-2">
|
||||
✅ Pedido concluído com sucesso
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Track progress for purchases */}
|
||||
{activeTab === 'compras' && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
{['Pendente', 'Pago', 'Faturado', 'Entregue'].map((status, idx) => {
|
||||
const isCompleted = ['Pendente', 'Pago', 'Faturado', 'Entregue'].indexOf(order.status) >= idx
|
||||
return (
|
||||
<div key={status} className="flex items-center">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold ${isCompleted
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-500'
|
||||
}`}>
|
||||
{isCompleted ? '✓' : idx + 1}
|
||||
</div>
|
||||
{idx < 3 && (
|
||||
<div className={`w-16 h-1 mx-1 ${['Pendente', 'Pago', 'Faturado', 'Entregue'].indexOf(order.status) > idx
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between mt-2 text-xs text-gray-500">
|
||||
<span>Pendente</span>
|
||||
<span>Pago</span>
|
||||
<span>Faturado</span>
|
||||
<span>Entregue</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
|
|
|
|||
Loading…
Reference in a new issue