+
Busca rápida
setSearch(e.target.value)}
- placeholder="Procure por nome ou SKU"
+ placeholder="Procure por nome"
className="mt-2 w-full rounded border border-gray-200 px-3 py-2"
/>
-
Otimizada para milhares de SKUs com renderização virtualizada.
-
-
- Princípio Ativo
- setFilters((prev) => ({ ...prev, activeIngredient: e.target.value }))}
- >
- Todos
- {actives.map((active) => (
-
- {active}
-
- ))}
-
-
-
- Categoria
- setFilters((prev) => ({ ...prev, category: e.target.value }))}
- >
- Todas
- {categories.map((category) => (
-
- {category}
-
- ))}
-
-
-
- Laboratório
- setFilters((prev) => ({ ...prev, lab: e.target.value }))}
- >
- Todos
- {labs.map((lab) => (
-
- {lab}
-
- ))}
-
-
-
- setFilters(defaultFilters)}
- >
- Limpar filtros
-
-
+
Nome
- Princípio Ativo
- Laboratório
+ Descrição
Lote
Validade
- Distribuidora
Preço
Ações
diff --git a/marketplace/src/pages/ProductSearch.tsx b/marketplace/src/pages/ProductSearch.tsx
new file mode 100644
index 0000000..da355ce
--- /dev/null
+++ b/marketplace/src/pages/ProductSearch.tsx
@@ -0,0 +1,149 @@
+import { useState, useEffect } from 'react'
+import { productService } from '../services/productService'
+import { LocationPicker } from '../components/LocationPicker'
+import { ProductCard } from '../components/ProductCard'
+import { ProductWithDistance } from '../types/product'
+import { useDebounce } from '../hooks/useDebounce' // Assuming this hook exists or I'll generic implement it
+
+const ProductSearch = () => {
+ const [lat, setLat] = useState
(-16.3285)
+ const [lng, setLng] = useState(-48.9534)
+ const [search, setSearch] = useState('')
+ const [maxDistance, setMaxDistance] = useState(10)
+ const [products, setProducts] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [total, setTotal] = useState(0)
+
+ // Debounce search term
+ const [debouncedSearch, setDebouncedSearch] = useState(search)
+ useEffect(() => {
+ const timer = setTimeout(() => setDebouncedSearch(search), 500)
+ return () => clearTimeout(timer)
+ }, [search])
+
+ useEffect(() => {
+ const fetchProducts = async () => {
+ setLoading(true)
+ try {
+ const data = await productService.search({
+ lat,
+ lng,
+ search: debouncedSearch,
+ max_distance: maxDistance,
+ page: 1,
+ page_size: 50,
+ })
+ setProducts(data.products)
+ setTotal(data.total)
+ } catch (err) {
+ console.error('Failed to fetch products', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchProducts()
+ }, [lat, lng, debouncedSearch, maxDistance])
+
+ const handleLocationSelect = (newLat: number, newLng: number) => {
+ setLat(newLat)
+ setLng(newLng)
+ }
+
+ const addToCart = (product: ProductWithDistance) => {
+ console.log('Added to cart:', product)
+ // Implement cart logic later
+ alert(`Adicionado ao carrinho: ${product.name}`)
+ }
+
+ return (
+
+
Encontre Medicamentos Próximos
+
+
+ {/* Filters & Map */}
+
+
+
Sua Localização
+
+
+ Clique no mapa para ajustar sua localização.
+
+
+
+
+
Filtros
+
+
+ Busca
+ setSearch(e.target.value)}
+ placeholder="Nome do medicamento..."
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary sm:text-sm p-2 border"
+ />
+
+
+
+
+ Raio de busca: {maxDistance} km
+
+ setMaxDistance(Number(e.target.value))}
+ className="w-full mt-2"
+ />
+
+
+
+
+ {/* Results */}
+
+
+
+ {loading ? 'Buscando...' : `${total} produtos encontrados`}
+
+
+ Ordenado por: Vencimento (mais próximo)
+
+
+
+ {loading ? (
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ {products.length === 0 && (
+
+
Nenhum produto encontrado nesta região.
+
Tente aumentar o raio de busca ou mudar a localização.
+
+ )}
+
+ )}
+
+
+
+ )
+}
+
+export default ProductSearch
diff --git a/marketplace/src/services/productService.ts b/marketplace/src/services/productService.ts
new file mode 100644
index 0000000..9fc2660
--- /dev/null
+++ b/marketplace/src/services/productService.ts
@@ -0,0 +1,24 @@
+import { apiClient } from './apiClient'
+import { ProductSearchFilters, ProductSearchPage } from '../types/product'
+
+export const productService = {
+ search: async (filters: ProductSearchFilters): Promise => {
+ // Build query params
+ const params = new URLSearchParams()
+
+ // Required
+ params.append('lat', filters.lat.toString())
+ params.append('lng', filters.lng.toString())
+
+ if (filters.search) params.append('search', filters.search)
+ if (filters.min_price) params.append('min_price', filters.min_price.toString())
+ if (filters.max_price) params.append('max_price', filters.max_price.toString())
+ if (filters.max_distance) params.append('max_distance', filters.max_distance.toString())
+ if (filters.expires_before) params.append('expires_before', filters.expires_before.toString())
+ if (filters.page) params.append('page', filters.page.toString())
+ if (filters.page_size) params.append('page_size', filters.page_size.toString())
+
+ const response = await apiClient.get(`/v1/products/search?${params.toString()}`)
+ return response.data
+ }
+}
diff --git a/marketplace/src/types/product.ts b/marketplace/src/types/product.ts
index 267ff32..64c8f4f 100644
--- a/marketplace/src/types/product.ts
+++ b/marketplace/src/types/product.ts
@@ -1,12 +1,37 @@
export interface Product {
id: string
+ seller_id: string
name: string
- activeIngredient: string
- lab: string
- category: string
+ description: string
batch: string
- expiry: string
- vendorId: string
- vendorName: string
- price: number
+ expires_at: string // ISO date
+ price_cents: number
+ stock: number
+ created_at: string
+ updated_at: string
+}
+
+export interface ProductWithDistance extends Product {
+ distance_km: number
+ tenant_city: string
+ tenant_state: string
+}
+
+export interface ProductSearchFilters {
+ search?: string
+ min_price?: number
+ max_price?: number
+ max_distance?: number
+ expires_before?: number
+ lat: number
+ lng: number
+ page?: number
+ page_size?: number
+}
+
+export interface ProductSearchPage {
+ products: ProductWithDistance[]
+ total: number
+ page: number
+ page_size: number
}