correção
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })} />
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user