- 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
337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { Header } from '@/components/Header'
|
|
import { Package, MapPin, Clock, CheckCircle, AlertCircle } from 'lucide-react'
|
|
|
|
interface OrderItem {
|
|
id: string
|
|
product_name: string
|
|
quantity: number
|
|
unit_price: number
|
|
}
|
|
|
|
interface Address {
|
|
street: string
|
|
number: string
|
|
city: string
|
|
state: string
|
|
zip: string
|
|
}
|
|
|
|
interface Order {
|
|
id: string
|
|
number: string
|
|
status: string
|
|
seller: {
|
|
id: string
|
|
name: string
|
|
company_name: string
|
|
}
|
|
items: OrderItem[]
|
|
shipping_address: Address
|
|
total_amount: number
|
|
created_at: string
|
|
ready_for_delivery_at?: string
|
|
}
|
|
|
|
export function DeliveryDashboardPage() {
|
|
const { user } = useAuth()
|
|
const [orders, setOrders] = useState<Order[]>([])
|
|
const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending')
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [expandedOrder, setExpandedOrder] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
setLoading(true)
|
|
setTimeout(() => {
|
|
setOrders([
|
|
{
|
|
id: '1',
|
|
number: 'PED-001',
|
|
status: 'ready_for_delivery',
|
|
seller: {
|
|
id: 'seller-1',
|
|
name: 'Farmácia Central',
|
|
company_name: 'Farmácia Central LTDA',
|
|
},
|
|
items: [
|
|
{ id: 'item-1', product_name: 'Dipirona 500mg', quantity: 5, unit_price: 15.90 },
|
|
{ id: 'item-2', product_name: 'Vitamina C 1000mg', quantity: 3, unit_price: 28.50 },
|
|
],
|
|
shipping_address: {
|
|
street: 'Rua das Flores',
|
|
number: '123',
|
|
city: 'São Paulo',
|
|
state: 'SP',
|
|
zip: '01234-567',
|
|
},
|
|
total_amount: 165.20,
|
|
created_at: '2024-02-25T10:30:00Z',
|
|
ready_for_delivery_at: '2024-02-26T08:00:00Z',
|
|
},
|
|
{
|
|
id: '2',
|
|
number: 'PED-002',
|
|
status: 'ready_for_delivery',
|
|
seller: {
|
|
id: 'seller-2',
|
|
name: 'Distribuidora MedPharma',
|
|
company_name: 'MedPharma Distribuidora LTDA',
|
|
},
|
|
items: [
|
|
{ id: 'item-3', product_name: 'Ibuprofeno 400mg', quantity: 10, unit_price: 12.30 },
|
|
{ id: 'item-4', product_name: 'Paracetamol 750mg', quantity: 8, unit_price: 8.90 },
|
|
],
|
|
shipping_address: {
|
|
street: 'Av. Paulista',
|
|
number: '1000',
|
|
city: 'São Paulo',
|
|
state: 'SP',
|
|
zip: '01311-100',
|
|
},
|
|
total_amount: 194.40,
|
|
created_at: '2024-02-25T14:20:00Z',
|
|
ready_for_delivery_at: '2024-02-26T09:00:00Z',
|
|
},
|
|
])
|
|
setLoading(false)
|
|
}, 800)
|
|
}, [])
|
|
|
|
const handleAcceptDelivery = (orderId: string) => {
|
|
setOrders(orders.map(order =>
|
|
order.id === orderId ? { ...order, status: 'in_transit' } : order
|
|
))
|
|
}
|
|
|
|
const handleStartDelivery = (orderId: string) => {
|
|
setOrders(orders.map(order =>
|
|
order.id === orderId ? { ...order, status: 'shipped' } : order
|
|
))
|
|
}
|
|
|
|
const handleCompleteDelivery = (orderId: string) => {
|
|
setOrders(orders.map(order =>
|
|
order.id === orderId ? { ...order, status: 'delivered' } : order
|
|
))
|
|
}
|
|
|
|
const pendingOrders = orders.filter(o => ['ready_for_delivery', 'in_transit'].includes(o.status))
|
|
const completedOrders = orders.filter(o => ['delivered', 'completed'].includes(o.status))
|
|
const displayOrders = activeTab === 'pending' ? pendingOrders : completedOrders
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100">
|
|
<Header />
|
|
|
|
<div className="mx-auto max-w-7xl px-4 py-6">
|
|
{/* Page Title */}
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900">Minhas Entregas</h1>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Olá, {user?.name} — gerencie suas entregas pendentes e histórico
|
|
</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="mb-6 flex gap-1 border-b border-gray-200">
|
|
<button
|
|
onClick={() => setActiveTab('pending')}
|
|
className={`pb-3 px-4 text-sm font-medium transition-colors ${activeTab === 'pending'
|
|
? 'border-b-2 text-white rounded-t-lg px-4 py-2'
|
|
: 'text-gray-500 hover:text-gray-900'
|
|
}`}
|
|
style={activeTab === 'pending' ? { borderColor: '#0F4C81', backgroundColor: '#0F4C81' } : {}}
|
|
>
|
|
Entregas Disponíveis ({pendingOrders.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('history')}
|
|
className={`pb-3 px-4 text-sm font-medium transition-colors ${activeTab === 'history'
|
|
? 'border-b-2 text-white rounded-t-lg px-4 py-2'
|
|
: 'text-gray-500 hover:text-gray-900'
|
|
}`}
|
|
style={activeTab === 'history' ? { borderColor: '#0F4C81', backgroundColor: '#0F4C81' } : {}}
|
|
>
|
|
Histórico ({completedOrders.length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* Loading */}
|
|
{loading && (
|
|
<div className="flex items-center justify-center py-16">
|
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-gray-200" style={{ borderTopColor: '#0F4C81' }} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mb-6 flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4">
|
|
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-600" />
|
|
<div>
|
|
<h3 className="font-semibold text-red-900">Erro ao carregar entregas</h3>
|
|
<p className="text-sm text-red-700">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!loading && displayOrders.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center rounded-xl bg-white py-16 shadow">
|
|
<Package className="mb-4 h-16 w-16 text-gray-300" />
|
|
<h3 className="text-lg font-semibold text-gray-700">
|
|
{activeTab === 'pending' ? 'Nenhuma entrega disponível' : 'Nenhuma entrega concluída'}
|
|
</h3>
|
|
<p className="mt-1 max-w-xs text-center text-sm text-gray-500">
|
|
{activeTab === 'pending'
|
|
? 'Você será notificado quando novas entregas estiverem prontas'
|
|
: 'Suas entregas concluídas aparecerão aqui'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Orders Grid */}
|
|
{!loading && displayOrders.length > 0 && (
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{displayOrders.map((order) => (
|
|
<div
|
|
key={order.id}
|
|
className="overflow-hidden rounded-xl bg-white shadow hover:shadow-md transition-shadow"
|
|
>
|
|
{/* Order Header */}
|
|
<div className="flex items-center justify-between px-5 py-4" style={{ backgroundColor: '#0F4C81' }}>
|
|
<div>
|
|
<h3 className="text-base font-bold text-white">{order.number}</h3>
|
|
<p className="text-xs text-white/70">{order.seller.company_name}</p>
|
|
</div>
|
|
<span className="text-xl font-bold text-white">
|
|
R$ {order.total_amount.toFixed(2)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Order Body */}
|
|
<div className="space-y-3 px-5 py-4">
|
|
{/* Status Badge */}
|
|
<div>
|
|
{order.status === 'ready_for_delivery' && (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-3 py-1 text-sm font-medium text-amber-800">
|
|
<Clock className="h-4 w-4" />
|
|
Pronto para Entrega
|
|
</span>
|
|
)}
|
|
{order.status === 'in_transit' && (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800">
|
|
<AlertCircle className="h-4 w-4" />
|
|
Saiu para Entrega
|
|
</span>
|
|
)}
|
|
{order.status === 'delivered' && (
|
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-800">
|
|
<CheckCircle className="h-4 w-4" />
|
|
Entregue
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Address */}
|
|
<div className="flex items-start gap-3 rounded-lg bg-gray-50 p-3">
|
|
<MapPin className="mt-0.5 h-5 w-5 flex-shrink-0" style={{ color: '#0F4C81' }} />
|
|
<div>
|
|
<p className="text-sm font-semibold text-gray-900">Endereço de Entrega</p>
|
|
<p className="text-sm text-gray-600">
|
|
{order.shipping_address.street}, {order.shipping_address.number}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{order.shipping_address.city}, {order.shipping_address.state} — {order.shipping_address.zip}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-500">
|
|
<span className="font-semibold text-gray-700">{order.items.length}</span> {order.items.length === 1 ? 'item' : 'itens'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Expandable Items */}
|
|
<div className="border-t border-gray-100">
|
|
<button
|
|
onClick={() => setExpandedOrder(expandedOrder === order.id ? null : order.id)}
|
|
className="flex w-full items-center justify-between px-5 py-3 text-sm font-medium text-gray-500 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<span>{expandedOrder === order.id ? 'Ocultar' : 'Ver'} detalhes dos itens</span>
|
|
<svg
|
|
className={`h-4 w-4 transition-transform ${expandedOrder === order.id ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{expandedOrder === order.id && (
|
|
<div className="border-t border-gray-100 bg-gray-50 px-5 py-4">
|
|
<div className="space-y-2">
|
|
{order.items.map((item) => (
|
|
<div key={item.id} className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-800">{item.product_name}</p>
|
|
<p className="text-xs text-gray-500">Qtd: {item.quantity}</p>
|
|
</div>
|
|
<p className="text-sm font-semibold text-gray-900">
|
|
R$ {(item.unit_price * item.quantity).toFixed(2)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
{activeTab === 'pending' && (
|
|
<div className="flex gap-3 border-t border-gray-100 bg-gray-50 px-5 py-4">
|
|
{order.status === 'ready_for_delivery' && (
|
|
<>
|
|
<button
|
|
onClick={() => handleAcceptDelivery(order.id)}
|
|
className="flex-1 rounded-lg bg-green-600 py-2 text-sm font-medium text-white hover:bg-green-700 transition-colors"
|
|
>
|
|
Aceitar Entrega
|
|
</button>
|
|
<button
|
|
disabled
|
|
className="flex-1 cursor-not-allowed rounded-lg bg-gray-200 py-2 text-sm font-medium text-gray-400"
|
|
>
|
|
Recusar
|
|
</button>
|
|
</>
|
|
)}
|
|
{order.status === 'in_transit' && (
|
|
<>
|
|
<button
|
|
onClick={() => handleStartDelivery(order.id)}
|
|
className="flex-1 rounded-lg py-2 text-sm font-medium text-white hover:opacity-90 transition-colors"
|
|
style={{ backgroundColor: '#0F4C81' }}
|
|
>
|
|
Saiu para Entrega
|
|
</button>
|
|
<button
|
|
onClick={() => handleCompleteDelivery(order.id)}
|
|
className="flex-1 rounded-lg bg-green-600 py-2 text-sm font-medium text-white hover:bg-green-700 transition-colors"
|
|
>
|
|
Marcar Entregue
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|