atualizacoes feitas: Ajuste de responsividade, Painel admin ajustado, Ajuste de centralizacao e alinhamento, Ajuste de icones
This commit is contained in:
parent
71d6a17dac
commit
202ac10a6b
18 changed files with 725 additions and 217 deletions
10
marketplace/package-lock.json
generated
10
marketplace/package-lock.json
generated
|
|
@ -15,6 +15,7 @@
|
|||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-window": "^1.8.10",
|
||||
|
|
@ -3625,6 +3626,15 @@
|
|||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-window": "^1.8.10",
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ export function Header() {
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<header className="bg-[#1E3A8A] text-white shadow-md border-b border-blue-800">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<header className="bg-[#0F4C81] text-white shadow-md border-b border-[#0a3a63]">
|
||||
<div className="mx-auto max-w-7xl px-3 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link to="/dashboard" className="flex items-center gap-2">
|
||||
|
|
@ -107,7 +107,7 @@ export function Header() {
|
|||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<nav className="md:hidden border-t border-white/20 px-4 py-2 flex gap-2 overflow-x-auto">
|
||||
<nav className="md:hidden border-t border-white/20 px-3 py-2 flex gap-1 overflow-x-auto">
|
||||
{navItems.map((item) => {
|
||||
const isActive = location.pathname === item.path
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ function CartDropdownContent() {
|
|||
const removeItem = useCartStore((state) => state.removeItem)
|
||||
const { totalValue } = useCartStore(selectCartSummary)
|
||||
|
||||
// Show max 4 items in preview
|
||||
const displayItems = items.slice(0, 4)
|
||||
const hiddenCount = items.length - displayItems.length
|
||||
|
||||
|
|
@ -67,6 +66,7 @@ function CartDropdownContent() {
|
|||
export function Shell({ children }: { children: React.ReactNode }) {
|
||||
const { user, logout } = useAuth()
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const profileMenuRef = useRef<HTMLDivElement | null>(null)
|
||||
const { totalItems: cartCount } = useCartStore(selectCartSummary)
|
||||
|
||||
|
|
@ -87,132 +87,254 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|||
setIsProfileOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Close mobile menu on resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) setIsMobileMenuOpen(false)
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<header className="flex items-center justify-between bg-medicalBlue px-6 py-4 text-white shadow-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src={logoImg} alt="SaveInMed" className="h-10 w-auto" />
|
||||
<div>
|
||||
<p className="text-lg font-semibold">SaveInMed</p>
|
||||
<p className="text-sm text-gray-100">
|
||||
{isAdmin ? 'Painel Administrativo' : isOwner ? 'Painel do Dono' : isEmployee ? 'Painel do Colaborador' : 'Marketplace B2B'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="flex items-center gap-4 text-sm font-medium">
|
||||
{isAdmin && (
|
||||
<Link to="/dashboard" className="hover:underline">
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showDashboard && (
|
||||
<Link to="/seller" className="hover:underline">
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showOrders && (
|
||||
<Link to="/orders" className="hover:underline">
|
||||
Meus Pedidos
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{showProducts && (
|
||||
<Link to="/inventory" className="hover:underline">
|
||||
Meus Produtos
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Cart with hover dropdown */}
|
||||
<div className="relative group">
|
||||
<Link
|
||||
to="/cart"
|
||||
className="relative hover:bg-white/10 p-2 rounded-lg transition-colors block"
|
||||
title="Carrinho"
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold">
|
||||
{cartCount > 99 ? '99+' : cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Dropdown on hover */}
|
||||
<div className="invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 absolute right-0 top-full z-50 mt-2 w-80 rounded-lg bg-white shadow-xl border border-gray-100">
|
||||
{cartCount === 0 ? (
|
||||
<div className="p-6 text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<p className="mt-2 text-sm font-medium text-gray-600">Seu carrinho está vazio</p>
|
||||
<p className="text-xs text-gray-400">Adicione produtos para começar</p>
|
||||
</div>
|
||||
) : (
|
||||
<CartDropdownContent />
|
||||
)}
|
||||
<header className="bg-medicalBlue text-white shadow-md relative z-40">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between py-3 md:py-4">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<img src={logoImg} alt="SaveInMed" className="h-8 w-auto md:h-10" />
|
||||
<div>
|
||||
<p className="text-base md:text-lg font-semibold leading-tight">SaveInMed</p>
|
||||
<p className="text-[10px] md:text-sm text-gray-100 leading-tight">
|
||||
{isAdmin ? 'Painel Administrativo' : isOwner ? 'Painel do Dono' : isEmployee ? 'Painel do Colaborador' : 'Marketplace B2B'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user && (
|
||||
<div className="relative flex items-center" ref={profileMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsProfileOpen((prev) => !prev)}
|
||||
className="flex items-center gap-3 rounded bg-white/10 px-3 py-2 text-left text-xs font-semibold hover:bg-white/20 whitespace-nowrap"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isProfileOpen}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-[11px] font-bold">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden md:flex items-center gap-4 text-sm font-medium">
|
||||
{isAdmin && (
|
||||
<Link to="/dashboard" className="hover:underline">Admin</Link>
|
||||
)}
|
||||
{showDashboard && (
|
||||
<Link to="/seller" className="hover:underline">Dashboard</Link>
|
||||
)}
|
||||
{showOrders && (
|
||||
<Link to="/orders" className="hover:underline">Meus Pedidos</Link>
|
||||
)}
|
||||
{showProducts && (
|
||||
<Link to="/inventory" className="hover:underline">Meus Produtos</Link>
|
||||
)}
|
||||
|
||||
{/* Cart with hover dropdown */}
|
||||
<div className="relative group">
|
||||
<Link
|
||||
to="/cart"
|
||||
className="relative hover:bg-white/10 p-2 rounded-lg transition-colors block"
|
||||
title="Carrinho"
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold">
|
||||
{cartCount > 99 ? '99+' : cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<div className="invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 absolute right-0 top-full z-50 mt-2 w-80 rounded-lg bg-white shadow-xl border border-gray-100">
|
||||
{cartCount === 0 ? (
|
||||
<div className="p-6 text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<p className="mt-2 text-sm font-medium text-gray-600">Seu carrinho está vazio</p>
|
||||
<p className="text-xs text-gray-400">Adicione produtos para começar</p>
|
||||
</div>
|
||||
) : (
|
||||
<CartDropdownContent />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start leading-tight">
|
||||
<span className="text-sm font-semibold">{user.name}</span>
|
||||
<span className="text-[10px] uppercase text-white/80">{user.role}</span>
|
||||
</div>
|
||||
<svg className="h-3 w-3 text-white/80" 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>
|
||||
{isProfileOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-52 rounded-md bg-white py-2 text-sm text-gray-700 shadow-lg">
|
||||
<Link
|
||||
to={profilePath}
|
||||
className="block px-4 py-2 hover:bg-gray-100"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
>
|
||||
Ver Perfil
|
||||
</Link>
|
||||
<Link
|
||||
to={settingsPath}
|
||||
className="block px-4 py-2 hover:bg-gray-100"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
>
|
||||
Configurações
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Profile */}
|
||||
{user && (
|
||||
<div className="relative flex items-center" ref={profileMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={logout}
|
||||
className="block w-full px-4 py-2 text-left text-red-600 hover:bg-red-50"
|
||||
onClick={() => setIsProfileOpen((prev) => !prev)}
|
||||
className="flex items-center gap-3 rounded bg-white/10 px-3 py-2 text-left text-xs font-semibold hover:bg-white/20 whitespace-nowrap"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isProfileOpen}
|
||||
>
|
||||
Sair
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-[11px] font-bold">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col items-start leading-tight">
|
||||
<span className="text-sm font-semibold">{user.name}</span>
|
||||
<span className="text-[10px] uppercase text-white/80">{user.role}</span>
|
||||
</div>
|
||||
<svg className="h-3 w-3 text-white/80" 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>
|
||||
{isProfileOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-52 rounded-md bg-white py-2 text-sm text-gray-700 shadow-lg">
|
||||
<Link
|
||||
to={profilePath}
|
||||
className="block px-4 py-2 hover:bg-gray-100"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
>
|
||||
Ver Perfil
|
||||
</Link>
|
||||
<Link
|
||||
to={settingsPath}
|
||||
className="block px-4 py-2 hover:bg-gray-100"
|
||||
onClick={() => setIsProfileOpen(false)}
|
||||
>
|
||||
Configurações
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={logout}
|
||||
className="block w-full px-4 py-2 text-left text-red-600 hover:bg-red-50"
|
||||
>
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Mobile: Cart + Hamburger */}
|
||||
<div className="flex items-center gap-1 md:hidden">
|
||||
<Link to="/cart" className="relative p-2 rounded-lg hover:bg-white/10 transition-colors">
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute top-0.5 right-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold">
|
||||
{cartCount > 99 ? '99+' : cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
|
||||
aria-label="Abrir menu"
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Dropdown */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-white/20 bg-medicalBlue">
|
||||
<div className="mx-auto max-w-7xl px-4 py-3 space-y-1">
|
||||
{isAdmin && (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
{showDashboard && (
|
||||
<Link
|
||||
to="/seller"
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
{showOrders && (
|
||||
<Link
|
||||
to="/orders"
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Meus Pedidos
|
||||
</Link>
|
||||
)}
|
||||
{showProducts && (
|
||||
<Link
|
||||
to="/inventory"
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Meus Produtos
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to="/search"
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Buscar Produtos
|
||||
</Link>
|
||||
|
||||
{/* User section */}
|
||||
{user && (
|
||||
<div className="pt-3 mt-2 border-t border-white/20 space-y-1">
|
||||
<div className="flex items-center gap-3 px-3 py-2">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white/20 text-sm font-bold shrink-0">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold truncate">{user.name}</p>
|
||||
<p className="text-xs text-white/70 uppercase">{user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={profilePath}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Ver Perfil
|
||||
</Link>
|
||||
<Link
|
||||
to={settingsPath}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm hover:bg-white/10 transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Configurações
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { logout(); setIsMobileMenuOpen(false) }}
|
||||
className="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-sm text-red-300 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main className="px-6 py-5">{children}</main>
|
||||
|
||||
<main className="mx-auto max-w-7xl px-4 py-6">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,50 @@
|
|||
import { useAuth } from '../context/AuthContext'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FaCartShopping } from 'react-icons/fa6'
|
||||
|
||||
export function EmployeeDashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
<div className="min-h-screen bg-gray-100 p-4 md:p-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="flex items-center justify-between rounded-lg bg-white p-6 shadow">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 rounded-lg bg-white p-4 md:p-6 shadow">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Painel do Colaborador</h1>
|
||||
<p className="text-gray-600">Bem-vindo, {user?.name}</p>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Painel do Colaborador</h1>
|
||||
<p className="text-gray-600 text-sm md:text-base">Bem-vindo, {user?.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link
|
||||
to="/search"
|
||||
className="rounded bg-green-600 px-4 py-2 font-bold text-white hover:bg-green-700"
|
||||
className="flex items-center gap-2 rounded bg-green-600 px-4 py-2 font-bold text-white hover:bg-green-700 text-sm"
|
||||
>
|
||||
🛒 Comprar Medicamentos
|
||||
<FaCartShopping className="h-4 w-4 text-white" />
|
||||
Comprar Medicamentos
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="rounded bg-red-600 px-4 py-2 font-bold text-white hover:bg-red-700"
|
||||
className="rounded bg-red-600 px-4 py-2 font-bold text-white hover:bg-red-700 text-sm"
|
||||
>
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-3">
|
||||
<Link to="/search" className="rounded-lg bg-white p-6 shadow hover:shadow-lg transition-shadow">
|
||||
<h3 className="text-lg font-bold text-green-600">🛒 Comprar Medicamentos</h3>
|
||||
<p className="mt-2 text-gray-600">Encontrar medicamentos próximos à venda.</p>
|
||||
<div className="mt-6 grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
|
||||
<Link to="/search" className="rounded-lg bg-white p-4 md:p-6 shadow hover:shadow-lg transition-shadow">
|
||||
<h3 className="flex items-center gap-2 text-base md:text-lg font-bold text-green-600">
|
||||
<FaCartShopping className="h-5 w-5 text-gray-900" />
|
||||
Comprar Medicamentos
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-600 text-sm">Encontrar medicamentos próximos à venda.</p>
|
||||
</Link>
|
||||
<div className="rounded-lg bg-white p-6 shadow">
|
||||
<h3 className="text-lg font-bold">Pedidos</h3>
|
||||
<p className="mt-2 text-gray-600">Gerenciar pedidos recebidos.</p>
|
||||
{/* Link to Orders */}
|
||||
<div className="rounded-lg bg-white p-4 md:p-6 shadow">
|
||||
<h3 className="text-base md:text-lg font-bold">Pedidos</h3>
|
||||
<p className="mt-2 text-gray-600 text-sm">Gerenciar pedidos recebidos.</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white p-6 shadow">
|
||||
<h3 className="text-lg font-bold">Estoque</h3>
|
||||
<p className="mt-2 text-gray-600">Consultar e ajustar estoque.</p>
|
||||
{/* Link to Inventory */}
|
||||
<div className="rounded-lg bg-white p-4 md:p-6 shadow">
|
||||
<h3 className="text-base md:text-lg font-bold">Estoque</h3>
|
||||
<p className="mt-2 text-gray-600 text-sm">Consultar e ajustar estoque.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,8 @@ export function ForgotPasswordPage() {
|
|||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
<div className="w-full max-w-[400px] overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||
{/* Blue Header with Logo (Igual ao Login) */}
|
||||
<div className="flex flex-col items-center bg-blue-600 pb-8 pt-10 text-white">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm">
|
||||
<img src={logoImg} alt="Logo" className="h-10 w-auto brightness-0 invert" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center bg-[#0F4C81] pb-8 pt-10 text-white">
|
||||
<img src={logoImg} alt="Logo" className="h-14 w-auto mb-4" />
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
|
@ -91,7 +89,7 @@ export function ForgotPasswordPage() {
|
|||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="seu@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -84,10 +84,8 @@ export function LoginPage() {
|
|||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
<div className="w-full max-w-[400px] overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||
{/* Blue Header with Logo */}
|
||||
<div className="flex flex-col items-center bg-blue-600 pb-8 pt-10 text-white">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm">
|
||||
<img src={logoImg} alt="Logo" className="h-10 w-auto brightness-0 invert" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center bg-[#0F4C81] pb-8 pt-10 text-white">
|
||||
<img src={logoImg} alt="Logo" className="h-14 w-auto mb-4" />
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
|
@ -114,7 +112,7 @@ export function LoginPage() {
|
|||
name="username"
|
||||
autoComplete="username"
|
||||
placeholder="seu@email.com"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
|
|
@ -133,7 +131,7 @@ export function LoginPage() {
|
|||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
placeholder="••••••••"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-10 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-10 pr-10 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -148,7 +146,7 @@ export function LoginPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Link to="/forgot-password" className="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
<Link to="/forgot-password" className="text-sm font-medium text-[#0F4C81] hover:opacity-80">
|
||||
Esqueceu a senha?
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -176,7 +174,7 @@ export function LoginPage() {
|
|||
<div className="mt-6 text-center text-sm">
|
||||
<p className="text-gray-500">
|
||||
Ainda não tem conta?{' '}
|
||||
<Link to="/register" className="font-semibold text-blue-600 hover:text-blue-500">
|
||||
<Link to="/register" className="font-semibold text-[#0F4C81] hover:opacity-80">
|
||||
Cadastre-se
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { FaCartShopping } from 'react-icons/fa6'
|
||||
import { AiOutlineFileDone } from 'react-icons/ai'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { apiClient } from '../services/apiClient'
|
||||
import { adminService } from '../services/adminService'
|
||||
|
|
@ -177,7 +179,7 @@ export function OrdersPage() {
|
|||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-2xl">🛒</span>
|
||||
<FaCartShopping className="h-5 w-5 text-gray-900" />
|
||||
<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>
|
||||
|
|
@ -195,7 +197,7 @@ export function OrdersPage() {
|
|||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-2xl">💰</span>
|
||||
<AiOutlineFileDone className="h-5 w-5 text-gray-900" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold">Pedidos Recebidos</p>
|
||||
<p className="text-xs text-gray-400">Vendas para outras farmácias</p>
|
||||
|
|
@ -243,9 +245,12 @@ export function OrdersPage() {
|
|||
|
||||
{!loading && orders.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<span className="text-5xl block mb-4">
|
||||
{activeTab === 'compras' ? '🛒' : '💰'}
|
||||
</span>
|
||||
<div className="flex justify-center mb-4">
|
||||
{activeTab === 'compras'
|
||||
? <FaCartShopping className="h-14 w-14 text-gray-900" />
|
||||
: <AiOutlineFileDone className="h-14 w-14 text-gray-900" />
|
||||
}
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-700">
|
||||
Nenhum pedido {activeTab === 'compras' ? 'feito' : 'recebido'} ainda
|
||||
</h3>
|
||||
|
|
|
|||
|
|
@ -83,10 +83,8 @@ export function RegisterPage() {
|
|||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||
<div className="w-full max-w-[400px] overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||
{/* Blue Header with Logo */}
|
||||
<div className="flex flex-col items-center bg-blue-600 pb-8 pt-10 text-white">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm">
|
||||
<img src={logoImg} alt="Logo" className="h-10 w-auto brightness-0 invert" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center bg-[#0F4C81] pb-8 pt-10 text-white">
|
||||
<img src={logoImg} alt="Logo" className="h-14 w-auto mb-4" />
|
||||
<h1 className="text-2xl font-bold">SaveInMed</h1>
|
||||
<p className="text-blue-100 text-sm">Plataforma B2B de Medicamentos</p>
|
||||
</div>
|
||||
|
|
@ -111,7 +109,7 @@ export function RegisterPage() {
|
|||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="Seu nome"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
|
|
@ -123,7 +121,7 @@ export function RegisterPage() {
|
|||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="usuario.exemplo"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
|
|
@ -135,7 +133,7 @@ export function RegisterPage() {
|
|||
<input
|
||||
type="email"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="seu@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
|
|
@ -148,7 +146,7 @@ export function RegisterPage() {
|
|||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-3 pr-10 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 pl-3 pr-10 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
|
|
@ -168,7 +166,7 @@ export function RegisterPage() {
|
|||
<input
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
className="w-full rounded-xl border border-gray-200 py-2.5 px-3 text-gray-800 placeholder-gray-400 focus:border-[#0F4C81] focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
|
|
@ -188,7 +186,7 @@ export function RegisterPage() {
|
|||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Já tem uma conta?{' '}
|
||||
<Link to="/login" className="font-medium text-blue-600 hover:text-blue-500 hover:underline">
|
||||
<Link to="/login" className="font-medium text-[#0F4C81] hover:opacity-80 hover:underline">
|
||||
Entrar
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FaCartShopping } from 'react-icons/fa6'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { apiClient } from '../services/apiClient'
|
||||
import { formatCents } from '../utils/format'
|
||||
|
|
@ -55,17 +56,18 @@ export function SellerDashboardPage() {
|
|||
return (
|
||||
<Shell>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-medicalBlue">Dashboard do Vendedor</h1>
|
||||
<p className="text-sm text-gray-600">Métricas e indicadores de performance</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Link
|
||||
to="/search"
|
||||
className="rounded bg-green-600 px-4 py-2 text-sm font-semibold text-white hover:bg-green-700"
|
||||
className="flex items-center gap-2 rounded bg-green-600 px-4 py-2 text-sm font-semibold text-white hover:bg-green-700"
|
||||
>
|
||||
🛒 Comprar Medicamentos
|
||||
<FaCartShopping className="h-4 w-4 text-white" />
|
||||
Comprar Medicamentos
|
||||
</Link>
|
||||
<button
|
||||
onClick={loadDashboard}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,238 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { adminService, Company, CreateCompanyRequest } from '../../services/adminService'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { adminService, Company, CompanyDocument, CreateCompanyRequest } from '../../services/adminService'
|
||||
import { isValidCNPJ, maskCNPJ } from '../../utils/validators'
|
||||
|
||||
// ─── Document Preview Modal ────────────────────────────────────────────────────
|
||||
interface DocPreviewModalProps {
|
||||
companyName: string
|
||||
doc: CompanyDocument
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function DocPreviewModal({ companyName, doc, onClose }: DocPreviewModalProps) {
|
||||
const backdropRef = useRef<HTMLDivElement>(null)
|
||||
const isImage = doc.document_type === 'image'
|
||||
const isPdf = doc.document_type === 'pdf'
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === backdropRef.current) onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={backdropRef}
|
||||
onClick={handleBackdropClick}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
style={{ animation: 'fadeIn 0.2s ease' }}
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-col rounded-xl bg-white shadow-2xl"
|
||||
style={{
|
||||
width: 'min(90vw, 900px)',
|
||||
height: 'min(90vh, 700px)',
|
||||
animation: 'slideUp 0.25s ease',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between rounded-t-xl border-b border-gray-200 bg-[#0F4C81] px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20">
|
||||
<svg className="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-blue-200 uppercase tracking-wide">Doc. Legal</p>
|
||||
<p className="text-sm font-semibold text-white truncate max-w-xs">{companyName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={doc.document_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 rounded-lg bg-white/10 px-3 py-1.5 text-xs font-medium text-white hover:bg-white/20 transition-colors"
|
||||
title="Abrir em nova aba"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Nova aba
|
||||
</a>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-white hover:bg-white/20 transition-colors"
|
||||
title="Fechar"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filename bar */}
|
||||
<div className="flex items-center gap-2 bg-gray-50 px-5 py-2 border-b border-gray-100">
|
||||
<svg className="h-4 w-4 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||
</svg>
|
||||
<span className="text-xs text-gray-500 truncate">{doc.file_name}</span>
|
||||
</div>
|
||||
|
||||
{/* Preview area */}
|
||||
<div className="flex-1 overflow-hidden rounded-b-xl bg-gray-100">
|
||||
{isPdf && (
|
||||
<iframe
|
||||
src={`${doc.document_url}#toolbar=0&navpanes=0&scrollbar=1`}
|
||||
title={`Documento legal de ${companyName}`}
|
||||
className="h-full w-full border-0"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
/>
|
||||
)}
|
||||
{isImage && (
|
||||
<div className="flex h-full items-center justify-center p-4">
|
||||
<img
|
||||
src={doc.document_url}
|
||||
alt={`Documento legal de ${companyName}`}
|
||||
className="max-h-full max-w-full rounded-lg object-contain shadow-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isPdf && !isImage && (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 text-center p-6">
|
||||
<svg className="h-16 w-16 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">Formato não suportado para preview</p>
|
||||
<p className="mt-1 text-xs text-gray-400">{doc.file_name}</p>
|
||||
</div>
|
||||
<a
|
||||
href={doc.document_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Abrir documento →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||
@keyframes slideUp { from { opacity: 0; transform: translateY(16px) } to { opacity: 1; transform: translateY(0) } }
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Doc Viewer Button ─────────────────────────────────────────────────────────
|
||||
interface DocButtonProps {
|
||||
company: Company
|
||||
onPreview: (company: Company, doc: CompanyDocument) => void
|
||||
}
|
||||
|
||||
function DocViewerButton({ company, onPreview }: DocButtonProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [noDoc, setNoDoc] = useState(false)
|
||||
|
||||
const hasDocumentHint = Boolean(company.document_url)
|
||||
|
||||
const handleClick = async () => {
|
||||
if (noDoc) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const doc = await adminService.getCompanyDocument(company.id)
|
||||
onPreview(company, doc)
|
||||
} catch (err: unknown) {
|
||||
const status = (err as { status?: number })?.status
|
||||
if (status === 404) {
|
||||
setNoDoc(true)
|
||||
} else {
|
||||
setError('Erro ao carregar documento')
|
||||
setTimeout(() => setError(null), 3000)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (noDoc || (!hasDocumentHint && !loading)) {
|
||||
return (
|
||||
<span className="group relative inline-flex">
|
||||
<button
|
||||
disabled
|
||||
aria-label="Documento não enviado"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-gray-300 cursor-not-allowed"
|
||||
title="Documento não enviado"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-all group-hover:opacity-100 shadow-lg">
|
||||
Documento não enviado
|
||||
<span className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-800" />
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<button
|
||||
disabled
|
||||
className="flex h-8 items-center justify-center gap-1 rounded-lg bg-red-50 px-2 text-xs text-red-500"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01M12 3a9 9 0 100 18A9 9 0 0012 3z" />
|
||||
</svg>
|
||||
Erro
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="group relative inline-flex">
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
aria-label="Visualizar documento legal"
|
||||
title="Visualizar documento legal"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-blue-500 hover:bg-blue-50 hover:text-blue-700 transition-all disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
{loading ? (
|
||||
<svg className="h-4 w-4 animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{!loading && (
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-all group-hover:opacity-100 shadow-lg">
|
||||
Ver Doc. Legal
|
||||
<span className="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-800" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main Page ─────────────────────────────────────────────────────────────────
|
||||
export function CompaniesPage() {
|
||||
const [companies, setCompanies] = useState<Company[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
|
@ -21,6 +252,12 @@ export function CompaniesPage() {
|
|||
state: 'GO'
|
||||
})
|
||||
const [cnpjError, setCnpjError] = useState('')
|
||||
const [docFile, setDocFile] = useState<File | null>(null)
|
||||
const [docUploading, setDocUploading] = useState(false)
|
||||
|
||||
// Document preview state
|
||||
const [previewCompany, setPreviewCompany] = useState<Company | null>(null)
|
||||
const [previewDoc, setPreviewDoc] = useState<CompanyDocument | null>(null)
|
||||
|
||||
const pageSize = 50
|
||||
|
||||
|
|
@ -52,8 +289,32 @@ export function CompaniesPage() {
|
|||
try {
|
||||
if (editingCompany) {
|
||||
await adminService.updateCompany(editingCompany.id, formData)
|
||||
// Upload doc if selected during edit
|
||||
if (docFile) {
|
||||
setDocUploading(true)
|
||||
try {
|
||||
await adminService.uploadCompanyDocument(editingCompany.id, docFile)
|
||||
} catch (err) {
|
||||
console.error('Error uploading doc:', err)
|
||||
alert('Empresa salva, mas houve um erro ao enviar o documento.')
|
||||
} finally {
|
||||
setDocUploading(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await adminService.createCompany(formData)
|
||||
const created = await adminService.createCompany(formData)
|
||||
// Upload doc right after creating
|
||||
if (docFile && created?.id) {
|
||||
setDocUploading(true)
|
||||
try {
|
||||
await adminService.uploadCompanyDocument(created.id, docFile)
|
||||
} catch (err) {
|
||||
console.error('Error uploading doc:', err)
|
||||
alert('Empresa criada, mas houve um erro ao enviar o documento.')
|
||||
} finally {
|
||||
setDocUploading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowModal(false)
|
||||
resetForm()
|
||||
|
|
@ -102,6 +363,7 @@ export function CompaniesPage() {
|
|||
const resetForm = () => {
|
||||
setEditingCompany(null)
|
||||
setCnpjError('')
|
||||
setDocFile(null)
|
||||
setFormData({
|
||||
cnpj: '',
|
||||
corporate_name: '',
|
||||
|
|
@ -119,6 +381,16 @@ export function CompaniesPage() {
|
|||
setShowModal(true)
|
||||
}
|
||||
|
||||
const openDocPreview = (company: Company, doc: CompanyDocument) => {
|
||||
setPreviewCompany(company)
|
||||
setPreviewDoc(doc)
|
||||
}
|
||||
|
||||
const closeDocPreview = () => {
|
||||
setPreviewCompany(null)
|
||||
setPreviewDoc(null)
|
||||
}
|
||||
|
||||
const filteredCompanies = companies.filter(c => {
|
||||
if (filterStatus === 'all') return true
|
||||
if (filterStatus === 'verified') return c.is_verified
|
||||
|
|
@ -156,7 +428,7 @@ export function CompaniesPage() {
|
|||
</div>
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
className="rounded-lg bg-[#0F4C81] px-4 py-2 text-sm font-medium text-white hover:bg-[#0a3a63]"
|
||||
>
|
||||
+ Nova Empresa
|
||||
</button>
|
||||
|
|
@ -165,32 +437,35 @@ export function CompaniesPage() {
|
|||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Razão Social</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">CNPJ</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Categoria</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Cidade</th>
|
||||
<th className="px-4 py-3 text-center text-sm font-medium">Verificada</th>
|
||||
<th className="px-4 py-3 text-center text-sm font-medium" title="Documento Legal Regulatório">
|
||||
Doc. Legal
|
||||
</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={6} className="py-8 text-center text-gray-500">
|
||||
<td colSpan={7} className="py-8 text-center text-gray-500">
|
||||
Carregando...
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredCompanies.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="py-8 text-center text-gray-500">
|
||||
<td colSpan={7} className="py-8 text-center text-gray-500">
|
||||
Nenhuma empresa encontrada
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredCompanies.map((company) => (
|
||||
<tr key={company.id} className="hover:bg-gray-50">
|
||||
<tr key={company.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-4 py-3 text-sm font-medium text-gray-900">{company.corporate_name}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{company.cnpj}</td>
|
||||
<td className="px-4 py-3">
|
||||
|
|
@ -202,14 +477,21 @@ export function CompaniesPage() {
|
|||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleVerify(company.id)}
|
||||
className={`rounded-full px-2 py-1 text-xs font-medium ${company.is_verified
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
className={`rounded-full px-2 py-1 text-xs font-medium transition-colors ${company.is_verified
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200'
|
||||
}`}
|
||||
>
|
||||
{company.is_verified ? '✓ Verificada' : 'Pendente'}
|
||||
</button>
|
||||
</td>
|
||||
{/* Doc. Legal column — lazy loaded on click */}
|
||||
<td className="px-4 py-3 text-center">
|
||||
<DocViewerButton
|
||||
company={company}
|
||||
onPreview={openDocPreview}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => openEdit(company)}
|
||||
|
|
@ -256,7 +538,7 @@ export function CompaniesPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{/* Edit/Create Modal */}
|
||||
{showModal && (
|
||||
<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 max-h-[90vh] overflow-y-auto">
|
||||
|
|
@ -361,6 +643,55 @@ export function CompaniesPage() {
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
{/* Document Upload */}
|
||||
{!editingCompany && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Documento Legal
|
||||
<span className="ml-1 text-xs text-gray-400 font-normal">(PDF ou imagem — alvará, CRF, etc.)</span>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="doc-upload"
|
||||
className={`mt-1 flex flex-col items-center justify-center w-full rounded-lg border-2 border-dashed px-4 py-5 cursor-pointer transition-colors ${docFile
|
||||
? 'border-green-400 bg-green-50'
|
||||
: 'border-gray-300 bg-gray-50 hover:border-[#0F4C81] hover:bg-blue-50'
|
||||
}`}
|
||||
>
|
||||
{docFile ? (
|
||||
<div className="flex items-center gap-2 text-green-700">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium truncate max-w-xs">{docFile.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); setDocFile(null) }}
|
||||
className="ml-1 text-red-400 hover:text-red-600"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1 text-gray-500">
|
||||
<svg className="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Clique para selecionar</span>
|
||||
<span className="text-xs text-gray-400">PDF, JPG ou PNG</span>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
id="doc-upload"
|
||||
type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
className="hidden"
|
||||
onChange={(e) => setDocFile(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<button
|
||||
|
|
@ -372,15 +703,31 @@ export function CompaniesPage() {
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
|
||||
disabled={docUploading}
|
||||
className="flex items-center gap-2 rounded bg-[#0F4C81] px-4 py-2 text-sm text-white hover:bg-[#0a3a63] disabled:opacity-60"
|
||||
>
|
||||
Salvar
|
||||
{docUploading && (
|
||||
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
)}
|
||||
{docUploading ? 'Enviando doc...' : 'Salvar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Document Preview Modal */}
|
||||
{previewCompany && previewDoc && (
|
||||
<DocPreviewModal
|
||||
companyName={previewCompany.corporate_name}
|
||||
doc={previewDoc}
|
||||
onClose={closeDocPreview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { FaUserGroup, FaBuilding, FaBoxOpen } from 'react-icons/fa6'
|
||||
import { FaPills } from 'react-icons/fa'
|
||||
import { apiClient } from '../../services/apiClient'
|
||||
|
||||
interface DashboardStats {
|
||||
|
|
@ -54,7 +56,6 @@ export function DashboardHome() {
|
|||
const loadStats = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// Load counts from each endpoint
|
||||
const [users, companies, products, orders] = await Promise.all([
|
||||
apiClient.get('/v1/users?page=1&page_size=1').catch(() => ({ total: 0 })),
|
||||
apiClient.get('/v1/companies').catch(() => ([])),
|
||||
|
|
@ -76,21 +77,21 @@ export function DashboardHome() {
|
|||
}
|
||||
|
||||
const cards = [
|
||||
{ title: 'Usuários', value: stats.totalUsers, icon: '👥', color: 'text-blue-600 bg-blue-50' },
|
||||
{ title: 'Empresas', value: stats.totalCompanies, icon: '🏢', color: 'text-indigo-600 bg-indigo-50' },
|
||||
{ title: 'Produtos', value: stats.totalProducts, icon: '💊', color: 'text-emerald-600 bg-emerald-50' },
|
||||
{ title: 'Pedidos', value: stats.totalOrders, icon: '📦', color: 'text-amber-600 bg-amber-50' }
|
||||
{ title: 'Usuários', value: stats.totalUsers, Icon: FaUserGroup },
|
||||
{ title: 'Empresas', value: stats.totalCompanies, Icon: FaBuilding },
|
||||
{ title: 'Produtos', value: stats.totalProducts, Icon: FaPills },
|
||||
{ title: 'Pedidos', value: stats.totalOrders, Icon: FaBoxOpen },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Visão Geral</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">Acompanhe os indicadores principais da plataforma.</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.title}
|
||||
|
|
@ -102,8 +103,8 @@ export function DashboardHome() {
|
|||
{loading ? '...' : card.value.toLocaleString('pt-BR')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-lg ${card.color} text-2xl`}>
|
||||
{card.icon}
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-100">
|
||||
<card.Icon className="h-6 w-6 text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -112,33 +113,33 @@ export function DashboardHome() {
|
|||
{/* Quick Actions */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-900">Ações Rápidas</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<a href="/dashboard/users" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
👥
|
||||
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
|
||||
<a href="/dashboard/users" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-[#0F4C81]/40 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 group-hover:bg-[#0F4C81] transition-colors">
|
||||
<FaUserGroup className="h-5 w-5 text-gray-900 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Usuários</h3>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-[#0F4C81]">Gerenciar Usuários</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Criar, editar e remover usuários do sistema</p>
|
||||
</a>
|
||||
<a href="/dashboard/companies" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
🏢
|
||||
<a href="/dashboard/companies" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-[#0F4C81]/40 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 group-hover:bg-[#0F4C81] transition-colors">
|
||||
<FaBuilding className="h-5 w-5 text-gray-900 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Empresas</h3>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-[#0F4C81]">Gerenciar Empresas</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Verificar e administrar farmácias parceiras</p>
|
||||
</a>
|
||||
<a href="/dashboard/products" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
💊
|
||||
<a href="/dashboard/products" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-[#0F4C81]/40 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 group-hover:bg-[#0F4C81] transition-colors">
|
||||
<FaPills className="h-5 w-5 text-gray-900 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Produtos</h3>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-[#0F4C81]">Gerenciar Produtos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Catálogo global de medicamentos</p>
|
||||
</a>
|
||||
<a href="/dashboard/orders" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-blue-200 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
📦
|
||||
<a href="/dashboard/orders" className="group flex flex-col rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-all hover:border-[#0F4C81]/40 hover:shadow-md">
|
||||
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 group-hover:bg-[#0F4C81] transition-colors">
|
||||
<FaBoxOpen className="h-5 w-5 text-gray-900 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-blue-700">Gerenciar Pedidos</h3>
|
||||
<h3 className="font-semibold text-gray-900 group-hover:text-[#0F4C81]">Gerenciar Pedidos</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Acompanhar fluxo de entregas</p>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ export function LogisticsPage() {
|
|||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<tr>
|
||||
<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">Transportadora</th>
|
||||
|
|
|
|||
|
|
@ -83,9 +83,9 @@ export function OrdersPage() {
|
|||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export function ProductsPage() {
|
|||
<h1 className="text-2xl font-bold text-gray-900">Produtos</h1>
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
className="rounded-lg bg-[#0F4C81] px-4 py-2 text-sm font-medium text-white hover:bg-[#0a3a63]"
|
||||
>
|
||||
+ Novo Produto
|
||||
</button>
|
||||
|
|
@ -169,7 +169,7 @@ export function ProductsPage() {
|
|||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm ring-1 ring-gray-950/5">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-[#1E3A8A] text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Produto</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold uppercase tracking-wider">Loja</th>
|
||||
|
|
@ -377,7 +377,7 @@ export function ProductsPage() {
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
|
||||
className="rounded bg-[#0F4C81] px-4 py-2 text-sm text-white hover:bg-[#0a3a63]"
|
||||
>
|
||||
Salvar
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ export function ReviewsPage() {
|
|||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<tr>
|
||||
<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">Pedido</th>
|
||||
|
|
|
|||
|
|
@ -114,16 +114,16 @@ export function UsersPage() {
|
|||
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
className="rounded-lg bg-[#0F4C81] px-4 py-2 text-sm font-medium text-white hover:bg-[#0a3a63]"
|
||||
>
|
||||
+ Novo Usuário
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||
<div className="overflow-x-auto rounded-lg bg-white shadow">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-blue-900 text-white">
|
||||
<thead className="bg-[#0F4C81] text-white">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Nome</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">Username</th>
|
||||
|
|
@ -205,7 +205,7 @@ export function UsersPage() {
|
|||
{/* Modal */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-4 md:p-6 shadow-xl mx-2">
|
||||
<h2 className="mb-4 text-xl font-bold">
|
||||
{editingUser ? 'Editar Usuário' : 'Novo Usuário'}
|
||||
</h2>
|
||||
|
|
@ -291,7 +291,7 @@ export function UsersPage() {
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
|
||||
className="rounded bg-[#0F4C81] px-4 py-2 text-sm text-white hover:bg-[#0a3a63]"
|
||||
>
|
||||
Salvar
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export interface Company {
|
|||
longitude: number
|
||||
city: string
|
||||
state: string
|
||||
document_url?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
|
@ -84,6 +85,12 @@ export interface UpdateCompanyRequest {
|
|||
state?: string
|
||||
}
|
||||
|
||||
export interface CompanyDocument {
|
||||
document_url: string
|
||||
document_type: string // 'pdf' | 'image' | 'other'
|
||||
file_name: string
|
||||
}
|
||||
|
||||
// ================== PRODUCTS ==================
|
||||
export interface Product {
|
||||
id: string
|
||||
|
|
@ -238,6 +245,22 @@ export const adminService = {
|
|||
return result
|
||||
},
|
||||
|
||||
getCompanyDocument: async (id: string) => {
|
||||
log('getCompanyDocument', { id })
|
||||
const result = await apiClient.get<CompanyDocument>(`/v1/companies/${id}/document`)
|
||||
log('getCompanyDocument result', result)
|
||||
return result
|
||||
},
|
||||
|
||||
uploadCompanyDocument: async (id: string, file: File) => {
|
||||
log('uploadCompanyDocument', { id, fileName: file.name })
|
||||
const formData = new FormData()
|
||||
formData.append('document', file)
|
||||
const result = await apiClient.postMultiPart<CompanyDocument>(`/v1/companies/${id}/document`, formData)
|
||||
log('uploadCompanyDocument result', result)
|
||||
return result
|
||||
},
|
||||
|
||||
// Products
|
||||
listProducts: async (page = 1, pageSize = 20) => {
|
||||
log('listProducts', { page, pageSize })
|
||||
|
|
|
|||
Loading…
Reference in a new issue