feat: Refactor app initialization to sequentially authenticate users and load shops from the database, removing mock data and local storage dependency for shops.
This commit is contained in:
@@ -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<Partial<State> | 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user