From 8842ff08b4faf6fb89d4bf0818d02f4736f9ccfb Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Thu, 12 Mar 2026 17:09:25 +0000 Subject: [PATCH] =?UTF-8?q?corre=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/ServiceList.tsx | 2 +- web/src/components/ShopCard.tsx | 3 -- web/src/context/AppContext.tsx | 5 +- web/src/pages/Booking.tsx | 13 ++++-- web/src/pages/Dashboard.tsx | 75 +++++++++++++++++++++++++++--- web/src/pages/Explore.tsx | 33 ++++--------- web/src/pages/ShopDetails.tsx | 19 ++++++-- web/src/types.ts | 2 +- 8 files changed, 105 insertions(+), 47 deletions(-) diff --git a/web/src/components/ServiceList.tsx b/web/src/components/ServiceList.tsx index f35b5c3..9395bf1 100644 --- a/web/src/components/ServiceList.tsx +++ b/web/src/components/ServiceList.tsx @@ -26,7 +26,7 @@ export const ServiceList = ({ {onSelect && ( )} diff --git a/web/src/components/ShopCard.tsx b/web/src/components/ShopCard.tsx index fd399e6..f4dc291 100644 --- a/web/src/components/ShopCard.tsx +++ b/web/src/components/ShopCard.tsx @@ -53,9 +53,6 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => { - ); diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx index f0e8e2d..61f52ea 100644 --- a/web/src/context/AppContext.tsx +++ b/web/src/context/AppContext.tsx @@ -128,6 +128,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { .map((b) => ({ id: b.id, name: b.name, + imageUrl: b.image_url ?? undefined, specialties: b.specialties ?? [], schedule: b.schedule ?? [], })), @@ -540,7 +541,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const addBarber: AppContextValue['addBarber'] = async (shopId, barber) => { const { error } = await supabase.from('barbers').insert([ - { shop_id: shopId, name: barber.name, specialties: barber.specialties } + { shop_id: shopId, name: barber.name, specialties: barber.specialties, image_url: barber.imageUrl } ]); if (error) { console.error("Erro addBarber:", error); @@ -550,7 +551,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const updateBarber: AppContextValue['updateBarber'] = async (shopId, barber) => { const { error } = await supabase.from('barbers').update({ - name: barber.name, specialties: barber.specialties + name: barber.name, specialties: barber.specialties, image_url: barber.imageUrl }).eq('id', barber.id); if (error) console.error("Erro updateBarber:", error); else await refreshShops(); diff --git a/web/src/pages/Booking.tsx b/web/src/pages/Booking.tsx index dc1c654..bda3253 100644 --- a/web/src/pages/Booking.tsx +++ b/web/src/pages/Booking.tsx @@ -4,8 +4,8 @@ * Gere um formulário multi-passo unificado para selecionar o Serviço, * Barbeiro, Data e Horário. Cruza disponibilidades em tempo real. */ -import { useNavigate, useParams } from 'react-router-dom'; -import { useMemo, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useMemo, useState, useEffect } from 'react'; import { Card } from '../components/ui/card'; import { Button } from '../components/ui/button'; import { Input } from '../components/ui/input'; @@ -15,6 +15,7 @@ import { currency } from '../lib/format'; export default function Booking() { const { id } = useParams<{ id: string }>(); + const [searchParams] = useSearchParams(); const navigate = useNavigate(); // Extração das ferramentas vitais do Context global da aplicação @@ -24,11 +25,17 @@ export default function Booking() { const shop = useMemo(() => shops.find((s) => s.id === id), [shops, id]); // Estados para as escolhas parciais do utilizador - const [serviceId, setService] = useState(''); + const [serviceId, setService] = useState(searchParams.get('service') || ''); const [barberId, setBarber] = useState(''); const [date, setDate] = useState(''); const [slot, setSlot] = useState(''); + // Sincroniza o serviceId se o parâmetro mudar (ex: navegação interna) + useEffect(() => { + const s = searchParams.get('service'); + if (s) setService(s); + }, [searchParams]); + const selectedService = shop?.services.find((s) => s.id === serviceId); const selectedBarber = shop?.barbers.find((b) => b.id === barberId); diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 411b5d1..641a201 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -122,6 +122,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) { deleteProduct, deleteService, deleteBarber, + updateBarber, updateShopDetails, } = useApp(); @@ -248,6 +249,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) { if (!barberName.trim()) return; addBarber(shop.id, { name: barberName, + imageUrl: '', // Foto será adicionada depois specialties: barberSpecs.split(',').map((s) => s.trim()).filter(Boolean), schedule: [], }); @@ -255,6 +257,41 @@ function DashboardInner({ shop }: { shop: BarberShop }) { setBarberSpecs(''); }; + const [uploadingBarberId, setUploadingBarberId] = useState(null); + + const handleBarberImageUpload = async (barberId: string, e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !shop) return; + setUploadingBarberId(barberId); + + try { + const fileExt = file.name.split('.').pop(); + const fileName = `${barberId}-${Math.random()}.${fileExt}`; + const filePath = `barbers/${fileName}`; + + const { error: uploadError } = await supabase.storage + .from('shops') + .upload(filePath, file); + + if (uploadError) throw uploadError; + + const { data } = supabase.storage + .from('shops') + .getPublicUrl(filePath); + + const barber = shop.barbers.find(b => b.id === barberId); + if (barber) { + await updateBarber(shop.id, { ...barber, imageUrl: data.publicUrl }); + alert('Foto do barbeiro atualizada!'); + } + } catch (error: any) { + console.error('Erro ao fazer upload da imagem do barbeiro:', error); + alert(`Erro: ${error?.message}`); + } finally { + setUploadingBarberId(null); + } + }; + const [isUploading, setIsUploading] = useState(false); const handleImageUpload = async (e: React.ChangeEvent) => { @@ -868,17 +905,41 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
{shop.barbers.map((b) => (
-
-

{b.name}

+
+
+
+
+ {b.imageUrl ? ( + {b.name} + ) : ( + + )} +
+ handleBarberImageUpload(b.id, e)} + /> + +
+
+

{b.name}

+

+ {b.specialties.length > 0 ? b.specialties.join(', ') : 'Sem especialidades'} +

+
+
-
-

- Especialidades: {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'} -

-
))} {shop.barbers.length === 0 && ( diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 56e2261..fe72cb4 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -17,8 +17,8 @@ export default function Explore() { // Estados para manter as seleções de filtragem const [query, setQuery] = useState(''); - const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'servicos'>('todas'); - const [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia'); + const [filter, setFilter] = useState<'todas' | 'top'>('todas'); + const [sortBy, setSortBy] = useState<'avaliacao' | 'servicos'>('avaliacao'); /** * Deriva a lista de Shops tratada a partir do conjunto mestre global. @@ -34,25 +34,14 @@ export default function Explore() { // Regra 2: Restrições de Chip const passesFilter = (shop: (typeof shops)[number]) => { if (filter === 'top') return (shop.rating || 0) >= 4.7; - if (filter === 'produtos') return (shop.products || []).length > 0; - if (filter === 'barbeiros') return (shop.barbers || []).length >= 2; - if (filter === 'servicos') return (shop.services || []).length >= 2; return true; }; - // Aplicação condicional com Sort const sorted = [...shops] .filter((shop) => matchesQuery(shop.name, shop.address || '')) .filter(passesFilter) .sort((a, b) => { if (sortBy === 'avaliacao') return (b.rating || 0) - (a.rating || 0); - if (sortBy === 'servicos') return (b.services || []).length - (a.services || []).length; - if (sortBy === 'preco') { - // Extrai o preço mínimo nos serviços oferecidos e compara - const aMin = (a.services || []).length ? Math.min(...a.services.map((s) => s.price)) : Infinity; - const bMin = (b.services || []).length ? Math.min(...b.services.map((s) => s.price)) : Infinity; - return aMin - bMin; - } // Critério por defeito ou quebra de empate: Avaliação descendente if (b.rating !== a.rating) return (b.rating || 0) - (a.rating || 0); @@ -91,9 +80,7 @@ export default function Explore() { onChange={(e) => setSortBy(e.target.value as typeof sortBy)} className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm" > - -
@@ -105,19 +92,15 @@ export default function Explore() { setFilter('top')}> Top avaliadas - setFilter('produtos')}> - Com produtos - - setFilter('barbeiros')}> - Mais barbeiros - - setFilter('servicos')}> - Mais serviços -
- {filtered.length === 0 ? ( + {!useApp().shopsReady ? ( +
+
+

A carregar barbearias...

+
+ ) : filtered.length === 0 ? (

Nenhuma barbearia encontrada

Tente ajustar a pesquisa ou limpar os filtros.

diff --git a/web/src/pages/ShopDetails.tsx b/web/src/pages/ShopDetails.tsx index 50360e2..a734ffc 100644 --- a/web/src/pages/ShopDetails.tsx +++ b/web/src/pages/ShopDetails.tsx @@ -16,11 +16,20 @@ import { Heart, MapPin, Maximize2, Star } from 'lucide-react'; export default function ShopDetails() { const { id } = useParams<{ id: string }>(); - const { shops, addToCart, toggleFavorite, isFavorite } = useApp(); + const { shops, shopsReady, addToCart, toggleFavorite, isFavorite } = useApp(); const shop = useMemo(() => shops.find((s) => s.id === id), [shops, id]); const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos'); const [imageOpen, setImageOpen] = useState(false); + if (!shopsReady) { + return ( +
+
+

A carregar detalhes...

+
+ ); + } + if (!shop) return
Barbearia não encontrada.
; const mapUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( `${shop.name} ${shop.address}` @@ -79,9 +88,6 @@ export default function ShopDetails() {
{(shop.services || []).length} serviços · {(shop.barbers || []).length} barbeiros
-
addToCart({ shopId: shop.id, type: 'service', refId: sid, qty: 1 })} + onSelect={(sid) => { + // Navega para a página de agendamento com o serviço pré-selecionado + window.location.href = `/agendar/${shop.id}?service=${sid}`; + }} /> ) : ( addToCart({ shopId: shop.id, type: 'product', refId: pid, qty: 1 })} /> diff --git a/web/src/types.ts b/web/src/types.ts index 30fd4a2..b4da3c7 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -1,4 +1,4 @@ -export type Barber = { id: string; name: string; specialties: string[]; schedule: { day: string; slots: string[] }[] }; +export type Barber = { id: string; name: string; imageUrl?: string; specialties: string[]; schedule: { day: string; slots: string[] }[] }; export type Service = { id: string; name: string; price: number; duration: number; barberIds: string[] }; export type Product = { id: string; name: string; price: number; stock: number }; export type BarberShop = { id: string; name: string; address: string; rating: number; services: Service[]; products: Product[]; barbers: Barber[]; imageUrl?: string };