diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx index 66a7183..279fe80 100644 --- a/web/src/context/AppContext.tsx +++ b/web/src/context/AppContext.tsx @@ -8,7 +8,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { nanoid } from 'nanoid'; import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types'; -import { mockShops, mockUsers } from '../data/mock'; +import { mockUsers } from '../data/mock'; import { storage } from '../lib/storage'; import { supabase } from '../lib/supabase'; @@ -66,7 +66,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const stored = storage.get | null>('smart-agenda', null); const safeStored = stored && typeof stored === 'object' ? stored : {}; const safeUsers = Array.isArray(safeStored.users) ? safeStored.users : initialState.users; - // shops are no longer bound by storage safely const safeAppointments = Array.isArray(safeStored.appointments) ? safeStored.appointments : initialState.appointments; const safeOrders = Array.isArray(safeStored.orders) ? safeStored.orders : initialState.orders; const safeCart = Array.isArray(safeStored.cart) ? safeStored.cart : initialState.cart; @@ -107,6 +106,10 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { storage.set('smart-agenda', state); }, [state]); + /** + * Hook de Inicialização Master — sequencial para evitar race condition. + * Ordem garantida: autenticar utilizador → carregar shops da BD → libertar UI. + */ useEffect(() => { let mounted = true; @@ -116,13 +119,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { /** * Sincroniza o Perfil do Utilizador a partir do Supabase Database ('profiles'). - * Cria automaticamente a shop ("Barbearia") se a Role determinar, integrando-o - * no estado (simulado em LocalStorage) das Shops. - * @param userId - UID emitido pela Supabase Auth. - * @param email - Endereço de Email (Payload do token). + * Cria automaticamente a shop se a Role for 'barbearia'. */ const applyProfile = async (userId: string, email?: string | null) => { - // Pedido restrito à tabela profiles de mapeamento Auth -> App Logic const { data, error } = await supabase .from('profiles') .select('id,name,role,shop_name') @@ -154,11 +153,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { : [...s.users, nextUser]; let shops = s.shops; - // Removida condicional restrita (/*role === 'barbearia' && shopId*/true) - // para garantir que *todas* as shops criadas por donos fiquem listadas no array maestro global - // para qualquer utilizador que faça fetch deste `id` if (data.shop_name) { - const shopIdFromOwner = userId; // O dono usa o user ID como shop ID + const shopIdFromOwner = userId; const exists = shops.some((shop) => shop.id === shopIdFromOwner); if (!exists) { @@ -174,24 +170,18 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; shops = [...shops, shop]; - // 🔹 INSERT REAL NA BD SUPABASE - // Garante que a mobile app e outros clients consigam obter esta nova loja + // 🔹 INSERT NA BD SUPABASE void supabase.from('shops').insert([ { id: shopIdFromOwner, name: shopName, address: 'Endereço a definir', - rating: 0 + rating: 0, } - ]).then(({ error }) => { - if (error) { - // Ignora o erro de duplicado (shop já existe na BD) - if (error.code !== '23505') { - console.error("Erro ao sincronizar shop nova na BD:", error); - } + ]).then(({ error: insertError }) => { + if (insertError && insertError.code !== '23505') { + console.error('Erro ao sincronizar shop nova na BD:', insertError); } - // Recarrega shops da BD para garantir que a nova shop aparece corretamente - void refreshShops(); }); } } @@ -206,28 +196,42 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; /** - * "Boot" Inicial: Verifica a sessão global com `getSession` de forma assíncrona. + * Boot sequencial: autenticar primeiro, depois carregar shops. + * Isso garante que quando o Dashboard renderiza, a shop já está no estado. */ const boot = async () => { - const { data } = await supabase.auth.getSession(); - if (!mounted) return; - const session = data.session; - if (session?.user) { - await applyProfile(session.user.id, session.user.email); - } else { - clearUser(); + try { + const { data } = await supabase.auth.getSession(); + if (!mounted) return; + + const session = data.session; + if (session?.user) { + // 1. Aplicar perfil (adiciona shop ao estado local se necessário) + await applyProfile(session.user.id, session.user.email); + // 2. Recarregar todas as shops da BD (garante sync completo) + if (mounted) await refreshShops(); + } else { + clearUser(); + // Mesmo sem user, carrega shops para o Explore page + if (mounted) await refreshShops(); + } + } catch (err) { + console.error('Erro durante o boot:', err); + } finally { + if (mounted) setLoading(false); } }; void boot(); /** - * Listener Global: Subscrição de eventos Auth que atualiza a context se - * utilizador fizer Log Out ou novo Login fora deste hook imediato. + * Listener de eventos Auth (login/logout em tempo real). */ - const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => { + const { data: sub } = supabase.auth.onAuthStateChange(async (_event, session) => { + if (!mounted) return; if (session?.user) { - void applyProfile(session.user.id, session.user.email); + await applyProfile(session.user.id, session.user.email); + if (mounted) await refreshShops(); } else { clearUser(); } @@ -267,7 +271,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { if (exists) return false; if (payload.role === 'barbearia') { - const userId = nanoid(); // Geramos o userId agora e usamos como shopId + const userId = nanoid(); const shopId = userId; const shop: BarberShop = { id: shopId, @@ -464,7 +468,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const updateShopDetails: AppContextValue['updateShopDetails'] = async (shopId, payload) => { - // DB Update const { error } = await supabase .from('shops') .update(payload) @@ -475,7 +478,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { return; } - // Local State Update setState((s) => ({ ...s, shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, ...payload } : shop)), @@ -509,19 +511,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { refreshShops, }; - /** - * Hook de Inicialização Master. - * Aciona a função de preenchimento do Contexto assincronamente e liberta - * a interface UI da view de Loading. - */ - useEffect(() => { - const init = async () => { - await refreshShops(); - setLoading(false); - }; - init(); - }, []); - // Loading Shield evita quebra generalizada se o app renderizar sem BD disponível if (loading) return null;