saveinmed/frontend/src/pages/dashboard/delivery/DeliveryDashboard.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

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>
)
}