agendamento
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user