diff --git a/web/src/components/CartPanel.tsx b/web/src/components/CartPanel.tsx index 463eac5..73809dc 100644 --- a/web/src/components/CartPanel.tsx +++ b/web/src/components/CartPanel.tsx @@ -14,9 +14,18 @@ export const CartPanel = () => { return acc; }, {}); - const handleCheckout = (shopId: string) => { + const handleCheckout = async (shopId: string) => { if (!user) return; - placeOrder(user.id, shopId); + try { + const order = await placeOrder(user.id, shopId); + if (order) { + alert("O seu pedido foi enviado à Barbearia com sucesso! Irá ser notificado brevemente."); + } else { + alert("Desculpe, ocorreu um erro a processar a sua encomenda."); + } + } catch (e) { + alert("Falha técnica no processo de Checkout."); + } }; return ( diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx index 2bd6c0b..16ff9bd 100644 --- a/web/src/context/AppContext.tsx +++ b/web/src/context/AppContext.tsx @@ -30,9 +30,9 @@ type AppContextValue = State & { removeFromCart: (refId: string) => void; clearCart: () => void; createAppointment: (input: Omit) => Promise; - placeOrder: (customerId: string, shopId?: string) => Order | null; + placeOrder: (customerId: string, shopId?: string) => Promise; updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise; - updateOrderStatus: (id: string, status: Order['status']) => void; + updateOrderStatus: (id: string, status: Order['status']) => Promise; addService: (shopId: string, service: Omit) => Promise; updateService: (shopId: string, service: Service) => Promise; deleteService: (shopId: string, serviceId: string) => Promise; @@ -97,6 +97,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const { data: barbersData } = await supabase.from('barbers').select('*'); const { data: productsData } = await supabase.from('products').select('*'); const { data: appointmentsData } = await supabase.from('appointments').select('*'); + const { data: ordersData } = await supabase.from('orders').select('*'); const fetchedShops: BarberShop[] = shopsData.map((shop) => ({ id: shop.id, @@ -142,6 +143,16 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { total: a.total, })); + const formattedOrders: Order[] = (ordersData ?? []).map((o) => ({ + id: o.id, + shopId: o.shop_id, + customerId: o.customer_id, + items: o.items, + total: o.total, + status: o.status as Order['status'], + createdAt: o.created_at, + })); + setState((s) => { // A BD é agora a única fonte de verdade. // Como o CRUD já insere na BD antes do refresh, a sobreposição com 'localVersion' foi @@ -165,7 +176,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return true; }); - return { ...s, shops: dedupedShops, appointments: formattedAppointments, shopsReady: true }; + return { ...s, shops: dedupedShops, appointments: formattedAppointments, orders: formattedOrders, shopsReady: true }; }); } catch (err) { console.error('refreshShops error:', err); @@ -402,7 +413,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; }; - const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => { + const placeOrder: AppContextValue['placeOrder'] = async (customerId, onlyShopId) => { if (!state.cart.length) return null; const grouped = state.cart.reduce>((acc, item) => { acc[item.shopId] = acc[item.shopId] || []; @@ -412,7 +423,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true)); - const newOrders: Order[] = entries.map(([shopId, items]) => { + const newOrders = await Promise.all(entries.map(async ([shopId, items]) => { const total = items.reduce((sum, item) => { const shop = state.shops.find((s) => s.id === item.shopId); if (!shop) return sum; @@ -423,11 +434,41 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return sum + price * item.qty; }, 0); - return { id: nanoid(), shopId, customerId, items, total, status: 'pendente', createdAt: new Date().toISOString() }; - }); + const dbOrder = { + shop_id: shopId, + customer_id: customerId, + items: items, + total: total, + status: 'pendente' + }; - setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] })); - return newOrders[0] ?? null; + const { data, error } = await supabase.from('orders').insert([dbOrder]).select().single(); + + if (error || !data) { + console.error("Erro ao criar encomenda na BD:", error); + return null; + } + + return { + id: data.id, + shopId: data.shop_id, + customerId: data.customer_id, + items: data.items, + total: data.total, + status: data.status, + createdAt: data.created_at + } as Order; + })); + + const successfulOrders = newOrders.filter((o): o is Order => o !== null); + + if (successfulOrders.length > 0) { + await refreshShops(); // Refresh app state to include new orders + setState((s) => ({ ...s, cart: [] })); + return successfulOrders[0]; + } + + return null; }; const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = async (id, status) => { @@ -439,8 +480,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { await refreshShops(); }; - const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => { - setState((s) => ({ ...s, orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)) })); + const updateOrderStatus: AppContextValue['updateOrderStatus'] = async (id, status) => { + const { error } = await supabase.from('orders').update({ status }).eq('id', id); + if (error) { + console.error("Erro ao atualizar status da encomenda:", error); + return; + } + await refreshShops(); }; const addService: AppContextValue['addService'] = async (shopId, service) => { diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 2461f81..411b5d1 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -50,6 +50,15 @@ import { CheckCircle2, } from 'lucide-react'; +type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings'; + +interface SidebarTab { + id: TabId; + label: string; + icon: React.FC; + badge?: number; +} + const periods: Record boolean> = { hoje: (d) => { const now = new Date(); @@ -69,7 +78,7 @@ const periods: Record boolean> = { const parseDate = (value: string) => new Date(value.replace(' ', 'T')); -type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings'; + export default function Dashboard() { const { user, shops, shopsReady } = useApp(); @@ -148,12 +157,16 @@ function DashboardInner({ shop }: { shop: BarberShop }) { // Pedidos apenas com produtos (não serviços) const shopOrders = orders.filter( - (o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) && o.items.some((item) => item.type === 'product') + (o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) ); - const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0); + const pendingOrders = orders.filter((o) => o.shopId === shop.id && o.status === 'pendente').length; const pendingAppts = shopAppointments.filter((a) => a.status === 'pendente').length; const confirmedAppts = shopAppointments.filter((a) => a.status === 'confirmado').length; + + + + const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0); const lowStock = shop.products.filter((p) => p.stock <= 3); const comparisonData = useMemo(() => { @@ -292,15 +305,15 @@ function DashboardInner({ shop }: { shop: BarberShop }) { } }; - const tabs = [ - { id: 'overview' as TabId, label: 'Visão Geral', icon: BarChart3 }, - { id: 'appointments' as TabId, label: 'Agendamentos', icon: Calendar }, - { id: 'history' as TabId, label: 'Histórico', icon: History }, - { id: 'orders' as TabId, label: 'Pedidos', icon: ShoppingBag }, - { id: 'services' as TabId, label: 'Serviços', icon: Scissors }, - { id: 'products' as TabId, label: 'Produtos', icon: Package }, - { id: 'barbers' as TabId, label: 'Barbeiros', icon: Users }, - { id: 'settings' as TabId, label: 'Definições', icon: Settings }, + const sidebarTabs: SidebarTab[] = [ + { id: 'overview', label: 'Visão Geral', icon: BarChart3 }, + { id: 'appointments', label: 'Agendamentos', icon: Calendar, badge: pendingAppts > 0 ? pendingAppts : undefined }, + { id: 'history', label: 'Histórico', icon: History }, + { id: 'orders', label: 'Pedidos', icon: ShoppingBag, badge: pendingOrders > 0 ? pendingOrders : undefined }, + { id: 'services', label: 'Serviços', icon: Scissors }, + { id: 'products', label: 'Produtos', icon: Package }, + { id: 'barbers', label: 'Barbeiros', icon: Users }, + { id: 'settings', label: 'Definições', icon: Settings }, ]; return ( @@ -328,7 +341,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) { {/* Tabs */} - ({ id: t.id, label: t.label }))} active={activeTab} onChange={(v) => setActiveTab(v as TabId)} /> + ({ id: t.id, label: t.label, badge: t.badge }))} active={activeTab} onChange={(v) => setActiveTab(v as TabId)} /> {/* Tab Content */} {activeTab === 'overview' && ( diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx index b3aa1b5..dd1ca6a 100644 --- a/web/src/pages/Profile.tsx +++ b/web/src/pages/Profile.tsx @@ -10,7 +10,6 @@ import { Badge } from '../components/ui/badge' import { currency } from '../lib/format' import { useApp } from '../context/AppContext' import { Calendar, ShoppingBag, User, Clock } from 'lucide-react' -import { listEvents, type EventRow } from '../lib/events' import { supabase } from '../lib/supabase' const statusColor: Record = { @@ -37,23 +36,11 @@ export default function Profile() { const [authId, setAuthId] = useState('') const [loadingAuth, setLoadingAuth] = useState(true) - // ✅ Eventos Supabase - const [events, setEvents] = useState([]) - const [loadingEvents, setLoadingEvents] = useState(true) - const [eventsError, setEventsError] = useState('') - - /** - * Obtém de forma segura o objeto de "getUser" fornecido pelo Supabase Auth. - * Não despoleta "flash" de erros se o utilizador não tiver credenciais, mas - * em caso positivo armazena Auth Id e Email globalmente na View. - */ useEffect(() => { let mounted = true - ; (async () => { const { data, error } = await supabase.auth.getUser() if (!mounted) return - if (error || !data.user) { setAuthEmail('') setAuthId('') @@ -61,42 +48,11 @@ export default function Profile() { setAuthEmail(data.user.email ?? '') setAuthId(data.user.id) } - setLoadingAuth(false) })() - - return () => { - mounted = false - } + return () => { mounted = false } }, []) - async function loadEvents() { - try { - setLoadingEvents(true) - setEventsError('') - const data = await listEvents() - setEvents(data) - } catch (e: any) { - setEventsError(e?.message || 'Erro ao carregar eventos') - } finally { - setLoadingEvents(false) - } - } - - // Carrega eventos ao entrar no perfil - useEffect(() => { - loadEvents() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // ✅ Recarrega eventos sempre que voltares para /perfil - useEffect(() => { - if (location.pathname === '/perfil') { - loadEvents() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.pathname]) - const myAppointments = useMemo(() => { if (!authId) return [] return appointments.filter((a) => a.customerId === authId) @@ -144,61 +100,12 @@ export default function Profile() {

Olá, {displayName}

{authEmail}

- Utilizador + Cliente Barber - {/* ✅ Supabase Events Section */} -
-
- -

Eventos (Supabase)

- - - {loadingEvents ? '…' : events.length} - - - - + Criar evento - -
- - {loadingEvents ? ( - -

A carregar eventos…

-
- ) : eventsError ? ( - -

{eventsError}

-
- ) : events.length === 0 ? ( - - -

Nenhum evento ainda

-

Cria um evento para aparecer aqui.

-
- ) : ( -
- {events.map((ev) => ( - -
-
-

{ev.title}

- {ev.description &&

{ev.description}

} -

{new Date(ev.date).toLocaleString()}

-
-
-
- ))} -
- )} -
- {/* Appointments Section */}