feat: atualizar tema para indigo, corrigir textos e implementar AppContext

This commit is contained in:
Rodrigo Lopes dos Santos
2026-03-15 10:49:05 +00:00
parent d63c27ec12
commit 8ece90a37e
14 changed files with 209 additions and 244 deletions

View File

@@ -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');