agendamento

This commit is contained in:
2026-03-11 10:06:41 +00:00
parent 1ad78609e2
commit 35c0eb4844
4 changed files with 143 additions and 28 deletions

View File

@@ -13,12 +13,14 @@ type State = {
user?: User;
shops: BarberShop[];
cart: CartItem[];
appointments: Appointment[];
};
type AppContextValue = State & {
logout: () => void;
addToCart: (item: CartItem) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
updateService: (shopId: string, service: Service) => Promise<void>;
deleteService: (shopId: string, serviceId: string) => Promise<void>;
@@ -29,6 +31,7 @@ const AppContext = createContext<AppContextValue | undefined>(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [shops, setShops] = useState<BarberShop[]>([]);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [cart, setCart] = useState<CartItem[]>([]);
const [user, setUser] = useState<User | undefined>(undefined);
const [loading, setLoading] = useState(true);
@@ -114,6 +117,31 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
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,
@@ -222,21 +250,60 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
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;
}
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,
logout,
addToCart,
clearCart,
createAppointment,
addService,
updateService,
deleteService,
refreshShops,
}),
[user, shops, cart]
[user, shops, cart, appointments]
);
// Loading Shield evita quebra generalizada se o app renderizar sem BD disponível

View File

@@ -4,7 +4,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 { Appointment, AppointmentStatus, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { mockUsers } from '../data/mock';
import { storage } from '../lib/storage';
import { supabase } from '../lib/supabase';
@@ -29,9 +29,9 @@ type AppContextValue = State & {
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
placeOrder: (customerId: string, shopId?: string) => Order | null;
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
updateOrderStatus: (id: string, status: Order['status']) => void;
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
updateService: (shopId: string, service: Service) => Promise<void>;
@@ -96,6 +96,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
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 fetchedShops: BarberShop[] = shopsData.map((shop) => ({
id: shop.id,
@@ -130,6 +131,17 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
})),
}));
const formattedAppointments: Appointment[] = (appointmentsData ?? []).map((a) => ({
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 AppointmentStatus,
total: a.total,
}));
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
@@ -153,7 +165,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return true;
});
return { ...s, shops: dedupedShops, shopsReady: true };
return { ...s, shops: dedupedShops, appointments: formattedAppointments, shopsReady: true };
});
} catch (err) {
console.error('refreshShops error:', err);
@@ -348,7 +360,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
const createAppointment: AppContextValue['createAppointment'] = (input) => {
const createAppointment: AppContextValue['createAppointment'] = async (input) => {
const shop = state.shops.find((s) => s.id === input.shopId);
if (!shop) return null;
const svc = shop.services.find((s) => s.id === input.serviceId);
@@ -359,9 +371,35 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
);
if (exists) return null;
const appointment: Appointment = { ...input, id: nanoid(), status: 'pendente', total: svc.price };
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
return appointment;
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;
}
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 AppointmentStatus,
total: newRow.total
};
};
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
@@ -392,8 +430,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return newOrders[0] ?? null;
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
setState((s) => ({ ...s, appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)) }));
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = async (id, status) => {
const { error } = await supabase.from('appointments').update({ status }).eq('id', id);
if (error) {
console.error("Erro ao atualizar status da marcação:", error);
return;
}
await refreshShops();
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {

View File

@@ -6,22 +6,27 @@ const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYm
const supabase = createClient(supabaseUrl, supabaseAnonKey);
async function check() {
console.log("Testing barber insert...");
console.log("Listing some tables...");
const shopId = 'e1e871f8-b88f-43c3-88de-e54b7b836ece'; // From previous output
// Auth with a test user to test RLS
await supabase.auth.signInWithPassword({
email: 'test_shop_1773154696588@test.com',
password: 'Password123!'
});
// First try standard insert
const { data: b1, error: e1 } = await supabase.from('barbers').insert([
{ shop_id: shopId, name: 'Test Barber Basic' }
]).select();
console.log("Insert without specialties:", e1 || "SUCCESS", b1);
const { data: b2, error: e2 } = await supabase.from('barbers').insert([
{ shop_id: shopId, name: 'Test Barber Complex', specialties: ['corte'] }
]).select();
console.log("Insert with specialties:", e2 || "SUCCESS", b2);
const tablesToCheck = ['appointments', 'events', 'bookings', 'orders'];
for (const t of tablesToCheck) {
const { error } = await supabase.from(t).select('id').limit(1);
if (error) {
console.log(`Table ${t} error:`, error.message);
} else {
console.log(`Table ${t} EXISTS!`);
const { data } = await supabase.from(t).select('*').limit(1);
if (data && data.length > 0) {
console.log(`${t} columns:`, Object.keys(data[0]));
}
}
}
}
check();

View File

@@ -85,7 +85,7 @@ export default function Booking() {
/**
* Dispara a ação de guardar a nova marcação na base de dados Supabase via Context API.
*/
const submit = () => {
const submit = async () => {
if (!user) {
// Bloqueia ações de clientes anónimos exigindo Sessão iniciada
navigate('/login');
@@ -94,12 +94,12 @@ export default function Booking() {
if (!canSubmit) return;
// O método 'createAppointment' fará internamente um pedido `supabase.from('appointments').insert(...)`
const appt = createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
const appt = await createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
if (appt) {
navigate('/perfil');
} else {
alert('Horário indisponível');
alert('Horário indisponível ou ocorreu um erro a comunicar com o servidor.');
}
};