correção

This commit is contained in:
2026-03-12 17:09:25 +00:00
parent 6ba3ecdc95
commit 8842ff08b4
8 changed files with 105 additions and 47 deletions

View File

@@ -26,7 +26,7 @@ export const ServiceList = ({
</div>
{onSelect && (
<Button onClick={() => onSelect(s.id)} size="sm" className="w-full">
Adicionar ao carrinho
Agendar
</Button>
)}
</Card>

View File

@@ -53,9 +53,6 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
<Button asChild variant="outline" size="sm" className="flex-1">
<Link to={`/barbearia/${shop.id}`}>Ver detalhes</Link>
</Button>
<Button asChild size="sm" className="flex-1 bg-amber-600 hover:bg-amber-700 border-0">
<Link to={`/agendar/${shop.id}`}>Agendar</Link>
</Button>
</div>
</Card>
);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<string | null>(null);
const handleBarberImageUpload = async (barberId: string, e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
@@ -868,17 +905,41 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
<div className="space-y-3 mb-6">
{shop.barbers.map((b) => (
<div key={b.id} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
<p className="font-bold text-slate-900 text-lg">{b.name}</p>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<div className="relative group/avatar">
<div className="w-16 h-16 rounded-full overflow-hidden border-2 border-slate-100 bg-slate-50 flex items-center justify-center">
{b.imageUrl ? (
<img src={b.imageUrl} alt={b.name} className="w-full h-full object-cover" />
) : (
<Users size={24} className="text-slate-400" />
)}
</div>
<input
type="file"
id={`barber-img-${b.id}`}
className="hidden"
accept="image/*"
onChange={(e) => handleBarberImageUpload(b.id, e)}
/>
<label
htmlFor={`barber-img-${b.id}`}
className="absolute inset-0 bg-black/40 text-white flex items-center justify-center rounded-full opacity-0 group-hover/avatar:opacity-100 cursor-pointer transition-opacity"
>
{uploadingBarberId === b.id ? <RefreshCw size={14} className="animate-spin" /> : <Plus size={14} />}
</label>
</div>
<div>
<p className="font-bold text-slate-900 text-lg">{b.name}</p>
<p className="text-sm text-slate-600">
{b.specialties.length > 0 ? b.specialties.join(', ') : 'Sem especialidades'}
</p>
</div>
</div>
<Button variant="danger" size="sm" onClick={() => deleteBarber(shop.id, b.id)}>
<Trash2 size={16} />
</Button>
</div>
<div className="space-y-1">
<p className="text-sm text-slate-600">
<span className="font-medium">Especialidades:</span> {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'}
</p>
</div>
</div>
))}
{shop.barbers.length === 0 && (

View File

@@ -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"
>
<option value="relevancia">Relevância</option>
<option value="avaliacao">Melhor avaliação</option>
<option value="preco">Menor preço</option>
<option value="servicos">Mais serviços</option>
</select>
</div>
@@ -105,19 +92,15 @@ export default function Explore() {
<Chip active={filter === 'top'} onClick={() => setFilter('top')}>
Top avaliadas
</Chip>
<Chip active={filter === 'produtos'} onClick={() => setFilter('produtos')}>
Com produtos
</Chip>
<Chip active={filter === 'barbeiros'} onClick={() => setFilter('barbeiros')}>
Mais barbeiros
</Chip>
<Chip active={filter === 'servicos'} onClick={() => setFilter('servicos')}>
Mais serviços
</Chip>
</div>
</Card>
{filtered.length === 0 ? (
{!useApp().shopsReady ? (
<div className="py-20 text-center">
<div className="inline-block w-8 h-8 border-4 border-slate-200 border-t-indigo-600 rounded-full animate-spin mb-2" />
<p className="text-sm text-slate-500">A carregar barbearias...</p>
</div>
) : filtered.length === 0 ? (
<Card className="p-8 text-center space-y-2">
<p className="text-lg font-semibold text-slate-900">Nenhuma barbearia encontrada</p>
<p className="text-sm text-slate-600">Tente ajustar a pesquisa ou limpar os filtros.</p>

View File

@@ -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 (
<div className="py-20 text-center">
<div className="inline-block w-8 h-8 border-4 border-slate-200 border-t-indigo-600 rounded-full animate-spin mb-2" />
<p className="text-sm text-slate-500">A carregar detalhes...</p>
</div>
);
}
if (!shop) return <div>Barbearia não encontrada.</div>;
const mapUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
`${shop.name} ${shop.address}`
@@ -79,9 +88,6 @@ export default function ShopDetails() {
<div className="text-sm text-slate-600">
{(shop.services || []).length} serviços · {(shop.barbers || []).length} barbeiros
</div>
<Button asChild>
<Link to={`/agendar/${shop.id}`}>Agendar</Link>
</Button>
</div>
<Tabs
tabs={[
@@ -94,7 +100,10 @@ export default function ShopDetails() {
{tab === 'servicos' ? (
<ServiceList
services={shop.services}
onSelect={(sid) => 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}`;
}}
/>
) : (
<ProductList products={shop.products} onAdd={(pid) => addToCart({ shopId: shop.id, type: 'product', refId: pid, qty: 1 })} />

View File

@@ -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 };