feat: atualizar tema para indigo, corrigir textos e implementar AppContext
This commit is contained in:
@@ -7,23 +7,34 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// Tipo interno determinando as propriedades globais partilhadas (Estados e Funções)
|
||||
type State = {
|
||||
user?: User;
|
||||
shops: BarberShop[];
|
||||
cart: CartItem[];
|
||||
appointments: Appointment[];
|
||||
orders: Order[];
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => boolean;
|
||||
logout: () => void;
|
||||
register: (payload: any) => boolean;
|
||||
addToCart: (item: CartItem) => void;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
|
||||
updateOrderStatus: (id: string, status: Order['status']) => Promise<void>;
|
||||
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
|
||||
updateService: (shopId: string, service: Service) => Promise<void>;
|
||||
deleteService: (shopId: string, serviceId: string) => Promise<void>;
|
||||
addProduct: (shopId: string, product: Omit<Product, 'id'>) => Promise<void>;
|
||||
updateProduct: (shopId: string, product: Product) => Promise<void>;
|
||||
deleteProduct: (shopId: string, productId: string) => Promise<void>;
|
||||
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => Promise<void>;
|
||||
updateBarber: (shopId: string, barber: Barber) => Promise<void>;
|
||||
deleteBarber: (shopId: string, barberId: string) => Promise<void>;
|
||||
refreshShops: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -32,135 +43,96 @@ const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [shops, setShops] = useState<BarberShop[]>([]);
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
/**
|
||||
* Hook executado no carregamento (mount) inicial.
|
||||
* Valida através de `supabase.auth.getUser()` se existe um token de sessão válido
|
||||
* (identificando o utilizador sem necessidade de o cliente refazer login ativamente).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
// Pedido restrito à API de autenticação do Supabase
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
let shopId: string | undefined = undefined;
|
||||
|
||||
// Vai buscar o shop_id mapeado na tabela profiles
|
||||
const { data: prof } = await supabase
|
||||
.from('profiles')
|
||||
.select('shop_id')
|
||||
.select('shop_id, role, name')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
|
||||
shopId = prof?.shop_id || undefined;
|
||||
|
||||
setUser({
|
||||
id: data.user.id,
|
||||
name: prof?.name || data.user.email?.split('@')[0] || 'Utilizador',
|
||||
email: data.user.email || '',
|
||||
role: 'barbearia', // assumido estaticamente na V1, deve vir de profiles
|
||||
shopId
|
||||
role: (prof?.role as any) || 'cliente',
|
||||
shopId: prof?.shop_id || undefined,
|
||||
} as User);
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Consulta mestra (Query) - Refresca todo o ecossistema de dados das barbearias.
|
||||
* Faz 2 queries (`supabase.from('shops').select('*')` e `services`) e depois
|
||||
* executa um JOIN manual via Javascript para injetar os serviços dentro
|
||||
* dos objetos da barbearia respetiva.
|
||||
*/
|
||||
const refreshShops = async () => {
|
||||
console.log("A buscar shops...");
|
||||
try {
|
||||
const { data: shopsData } = await supabase.from('shops').select('*');
|
||||
const { data: servicesData } = await supabase.from('services').select('*');
|
||||
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('*');
|
||||
|
||||
// Query 1: Obtém a listagem completa (tabela 'shops')
|
||||
const { data: shopsData, error: shopsError } = await supabase
|
||||
.from('shops')
|
||||
.select('*');
|
||||
if (shopsData) {
|
||||
const merged: BarberShop[] = shopsData.map((shop: any) => ({
|
||||
...shop,
|
||||
services: (servicesData || []).filter((s: any) => s.shop_id === shop.id).map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
price: s.price,
|
||||
duration: s.duration,
|
||||
barberIds: s.barber_ids || [],
|
||||
})),
|
||||
products: (productsData || []).filter((p: any) => p.shop_id === shop.id),
|
||||
barbers: (barbersData || []).filter((b: any) => b.shop_id === shop.id).map((b: any) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
specialties: b.specialties || [],
|
||||
schedule: b.schedule || [],
|
||||
})),
|
||||
}));
|
||||
setShops(merged);
|
||||
}
|
||||
|
||||
if (shopsError) {
|
||||
console.error("Erro ao buscar shops:", shopsError);
|
||||
return;
|
||||
if (appointmentsData) {
|
||||
setAppointments(
|
||||
appointmentsData.map((a: any) => ({
|
||||
id: a.id,
|
||||
shopId: a.shop_id,
|
||||
serviceId: a.service_id,
|
||||
barberId: a.barber_id,
|
||||
customerId: a.customer_id,
|
||||
date: a.date,
|
||||
status: a.status as Appointment['status'],
|
||||
total: a.total,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (ordersData) {
|
||||
setOrders(
|
||||
ordersData.map((o: any) => ({
|
||||
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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing shops:', err);
|
||||
}
|
||||
|
||||
// Query 2: Obtém a listagem associada globalmente (tabela 'services')
|
||||
const { data: servicesData, error: servicesError } = await supabase
|
||||
.from('services')
|
||||
.select('*');
|
||||
|
||||
if (servicesError) {
|
||||
console.error("Erro ao buscar services:", servicesError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 3: Obtém a listagem global de Barbeiros (tabela 'barbers')
|
||||
const { data: barbersData, error: barbersError } = await supabase
|
||||
.from('barbers')
|
||||
.select('*');
|
||||
|
||||
if (barbersError) {
|
||||
console.error("Erro ao buscar barbers:", barbersError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 4: Obtém a listagem global de Produtos (tabela 'products')
|
||||
const { data: productsData, error: productsError } = await supabase
|
||||
.from('products')
|
||||
.select('*');
|
||||
|
||||
if (productsError) {
|
||||
console.error("Erro ao buscar products:", productsError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 5: Obtém a listagem global de Marcações (tabela 'appointments')
|
||||
const { data: appointmentsData, error: appointmentsError } = await supabase
|
||||
.from('appointments')
|
||||
.select('*');
|
||||
|
||||
if (appointmentsError) {
|
||||
console.error("Erro ao buscar appointments:", appointmentsError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (appointmentsData) {
|
||||
setAppointments(
|
||||
appointmentsData.map((a: any) => ({
|
||||
id: a.id,
|
||||
shopId: a.shop_id,
|
||||
serviceId: a.service_id,
|
||||
barberId: a.barber_id,
|
||||
customerId: a.customer_id,
|
||||
date: a.date,
|
||||
status: a.status as Appointment['status'],
|
||||
total: a.total,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Associar serviços, barbeiros e produtos às respetivas shops, simulando um INNER JOIN nativo do SQL
|
||||
const shopsWithServices = shopsData.map((shop) => ({
|
||||
...shop,
|
||||
// Relaciona a 'foreign key' (shop_id) com o resgistro primário (shop.id)
|
||||
services: servicesData.filter((s) => s.shop_id === shop.id),
|
||||
products: productsData.filter((p) => p.shop_id === shop.id),
|
||||
barbers: barbersData.filter((b) => b.shop_id === shop.id),
|
||||
}));
|
||||
|
||||
console.log("Shops carregadas:", shopsWithServices);
|
||||
|
||||
setShops(shopsWithServices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook de Inicialização Master.
|
||||
* Aciona a função de preenchimento do Contexto assincronamente e liberta
|
||||
* a interface UI da view de Loading (`setLoading(false)`).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await refreshShops();
|
||||
@@ -169,150 +141,136 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Encerra a sessão JWT ativa com o Supabase Auth.
|
||||
* Limpa integralmente a interface do User local (estado React vazio).
|
||||
*/
|
||||
const login = (email: string, password: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setUser(undefined);
|
||||
};
|
||||
|
||||
// Funções elementares do fluxo transacional não persistido (Estado do Carrinho transitório/local)
|
||||
const register = (payload: any) => {
|
||||
const id = nanoid();
|
||||
const newUser: User = { ...payload, id };
|
||||
setUser(newUser);
|
||||
return true;
|
||||
};
|
||||
|
||||
const addToCart = (item: CartItem) => {
|
||||
setCart((prev) => [...prev, item]);
|
||||
setCart((prev: CartItem[]) => [...prev, item]);
|
||||
};
|
||||
|
||||
const clearCart = () => setCart([]);
|
||||
|
||||
// 🔹 CRUD SERVICES (SUPABASE REAL)
|
||||
|
||||
/**
|
||||
* Executa um INSERT na BD (via API REST gerada) protegendo interações com a tabela estrita 'services'.
|
||||
* @param {string} shopId - A foreign key relacionando o estabelecimento.
|
||||
* @param {Omit<Service, 'id'>} service - O DTO (Data Transfer Object) sem a primary key autonumerável.
|
||||
*/
|
||||
const addService = async (shopId: string, service: Omit<Service, 'id'>) => {
|
||||
// Insere os campos exatos formatados estritamente na query Supabase
|
||||
const { error } = await supabase.from('services').insert([
|
||||
{
|
||||
shop_id: shopId,
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
},
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao adicionar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Para manter integridade reativa pura, força refetch dos dados pós-mutação
|
||||
await supabase.from('services').insert([{ shop_id: shopId, ...service }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executa um UPDATE num tuplo específico filtrando analiticamente pela primary key `(eq('id', service.id))`.
|
||||
*/
|
||||
const updateService = async (shopId: string, service: Service) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.update({
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
})
|
||||
.eq('id', service.id); // Identificador vital do update
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao atualizar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, ...data } = service;
|
||||
await supabase.from('services').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executa uma instrução SQL DELETE remota rígida, baseada no ID unívoco do tuplo.
|
||||
*/
|
||||
const deleteService = async (shopId: string, serviceId: string) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.delete()
|
||||
.eq('id', serviceId);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao apagar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase.from('services').delete().eq('id', serviceId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const createAppointment: AppContextValue['createAppointment'] = async (input) => {
|
||||
const shop = shops.find((s) => s.id === input.shopId);
|
||||
if (!shop) return null;
|
||||
const svc = shop.services.find((s) => s.id === input.serviceId);
|
||||
if (!svc) return null;
|
||||
|
||||
const { data: newRow, error } = await supabase.from('appointments').insert([
|
||||
{
|
||||
shop_id: input.shopId,
|
||||
service_id: input.serviceId,
|
||||
barber_id: input.barberId,
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total: svc.price,
|
||||
}
|
||||
]).select().single();
|
||||
|
||||
if (error || !newRow) {
|
||||
console.error("Erro ao criar marcação na BD:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const addProduct = async (shopId: string, product: Omit<Product, 'id'>) => {
|
||||
await supabase.from('products').insert([{ shop_id: shopId, ...product }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateProduct = async (shopId: string, product: Product) => {
|
||||
const { id, ...data } = product;
|
||||
await supabase.from('products').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const deleteProduct = async (shopId: string, productId: string) => {
|
||||
await supabase.from('products').delete().eq('id', productId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const addBarber = async (shopId: string, barber: Omit<Barber, 'id'>) => {
|
||||
await supabase.from('barbers').insert([{ shop_id: shopId, ...barber }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateBarber = async (shopId: string, barber: Barber) => {
|
||||
const { id, ...data } = barber;
|
||||
await supabase.from('barbers').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const deleteBarber = async (shopId: string, barberId: string) => {
|
||||
await supabase.from('barbers').delete().eq('id', barberId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const createAppointment = async (input: Omit<Appointment, 'id' | 'status' | 'total'>) => {
|
||||
const svc = shops.flatMap(s => s.services).find(s => s.id === input.serviceId);
|
||||
const total = svc ? svc.price : 0;
|
||||
const { data } = await supabase.from('appointments').insert([{
|
||||
shop_id: input.shopId,
|
||||
service_id: input.serviceId,
|
||||
barber_id: input.barberId,
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total
|
||||
}]).select().single();
|
||||
await refreshShops();
|
||||
return data as any as Appointment;
|
||||
};
|
||||
|
||||
const updateAppointmentStatus = async (id: string, status: Appointment['status']) => {
|
||||
await supabase.from('appointments').update({ status }).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateOrderStatus = async (id: string, status: Order['status']) => {
|
||||
await supabase.from('orders').update({ status }).eq('id', id);
|
||||
await refreshShops();
|
||||
|
||||
return {
|
||||
id: newRow.id,
|
||||
shopId: newRow.shop_id,
|
||||
serviceId: newRow.service_id,
|
||||
barberId: newRow.barber_id,
|
||||
customerId: newRow.customer_id,
|
||||
date: newRow.date,
|
||||
status: newRow.status as Appointment['status'],
|
||||
total: newRow.total,
|
||||
};
|
||||
};
|
||||
|
||||
// Empacotamento em objeto estabilizado memoizado face renderizações espúrias (React Context Pattern)
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
shops,
|
||||
cart,
|
||||
appointments,
|
||||
orders,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
refreshShops,
|
||||
}),
|
||||
[user, shops, cart, appointments]
|
||||
[user, shops, cart, appointments, orders]
|
||||
);
|
||||
|
||||
// Loading Shield evita quebra generalizada se o app renderizar sem BD disponível
|
||||
if (loading) return null;
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook prático de acesso central sem import múltiplo do 'useContext' em toda aplicação
|
||||
export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
|
||||
Reference in New Issue
Block a user