From 35c0eb4844d8c2e20c2581fb6a80176f0e44853f Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Wed, 11 Mar 2026 10:06:41 +0000 Subject: [PATCH] agendamento --- src/context/AppContext.tsx | 69 +++++++++++++++++++++++++++++++++- web/src/context/AppContext.tsx | 63 ++++++++++++++++++++++++++----- web/src/lib/check_db.ts | 33 +++++++++------- web/src/pages/Booking.tsx | 6 +-- 4 files changed, 143 insertions(+), 28 deletions(-) diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index f7eeda8..513dd86 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -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) => Promise; addService: (shopId: string, service: Omit) => Promise; updateService: (shopId: string, service: Service) => Promise; deleteService: (shopId: string, serviceId: string) => Promise; @@ -29,6 +31,7 @@ const AppContext = createContext(undefined); export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [shops, setShops] = useState([]); + const [appointments, setAppointments] = useState([]); const [cart, setCart] = useState([]); const [user, setUser] = useState(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 diff --git a/web/src/context/AppContext.tsx b/web/src/context/AppContext.tsx index c4d3f61..2bd6c0b 100644 --- a/web/src/context/AppContext.tsx +++ b/web/src/context/AppContext.tsx @@ -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 | null; + createAppointment: (input: Omit) => Promise; placeOrder: (customerId: string, shopId?: string) => Order | null; - updateAppointmentStatus: (id: string, status: Appointment['status']) => void; + updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise; updateOrderStatus: (id: string, status: Order['status']) => void; addService: (shopId: string, service: Omit) => Promise; updateService: (shopId: string, service: Service) => Promise; @@ -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) => { diff --git a/web/src/lib/check_db.ts b/web/src/lib/check_db.ts index 82cdbab..71753e7 100644 --- a/web/src/lib/check_db.ts +++ b/web/src/lib/check_db.ts @@ -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(); diff --git a/web/src/pages/Booking.tsx b/web/src/pages/Booking.tsx index 76c5e42..dc1c654 100644 --- a/web/src/pages/Booking.tsx +++ b/web/src/pages/Booking.tsx @@ -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.'); } };