feat: add event creation functionality and enhance profile and shop details pages

- Implemented event creation feature with form validation in EventsCreate component.
- Updated Profile page to include navigation to create events.
- Enhanced ShopDetails page with improved layout, additional tabs for services, barbers, products, and details.
- Added loading states and error handling for asynchronous operations.
- Refactored styles for better UI consistency and responsiveness.
- Updated types to include new properties for BarberShop and added EventRow type.
This commit is contained in:
2026-05-06 11:00:50 +01:00
parent f9e5183a20
commit a065130167
13 changed files with 1713 additions and 532 deletions

View File

@@ -7,14 +7,16 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User, WaitlistEntry, AppNotification } from '../types';
import { supabase } from '../lib/supabase';
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
type State = {
user?: User;
shops: BarberShop[];
shopsReady: boolean;
cart: CartItem[];
favorites: string[];
appointments: Appointment[];
orders: Order[];
waitlists: WaitlistEntry[];
@@ -22,12 +24,14 @@ type State = {
};
type AppContextValue = State & {
login: (email: string, password: string) => User | undefined;
logout: () => void;
register: (payload: any) => User | undefined;
login: (email: string, password: string) => Promise<User | undefined>;
logout: () => Promise<void>;
register: (payload: any) => Promise<User | undefined>;
toggleFavorite: (shopId: string) => void;
isFavorite: (shopId: string) => boolean;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
placeOrder: (customerId: string, shopId: string) => Order | null;
placeOrder: (customerId: string, shopId: string) => Promise<Order | null>;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
@@ -53,37 +57,47 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [orders, setOrders] = useState<Order[]>([]);
const [cart, setCart] = useState<CartItem[]>([]);
const [favorites, setFavorites] = useState<string[]>([]);
const [waitlists, setWaitlists] = useState<WaitlistEntry[]>([]);
const [notifications, setNotifications] = useState<AppNotification[]>([]);
const [user, setUser] = useState<User | undefined>(undefined);
const [shopsReady, setShopsReady] = useState(false);
const [loading, setLoading] = useState(true);
const applySupabaseUser = async (authUser: any): Promise<User | undefined> => {
if (!authUser) return undefined;
const { data: prof } = await supabase
.from('profiles')
.select('shop_id, role, name, fcm_token')
.eq('id', authUser.id)
.single();
const nextUser: User = {
id: authUser.id,
name: prof?.name || authUser.user_metadata?.name || authUser.email?.split('@')[0] || 'Utilizador',
email: authUser.email || '',
password: '',
role: (prof?.role as any) || authUser.user_metadata?.role || 'cliente',
shopId: prof?.shop_id || undefined,
fcmToken: prof?.fcm_token || undefined,
};
setUser(nextUser);
registerForPushNotificationsAsync().then((token) => {
if (token && token !== prof?.fcm_token) {
supabase.from('profiles').update({ fcm_token: token }).eq('id', authUser.id).then();
}
});
return nextUser;
};
useEffect(() => {
const loadUser = async () => {
const { data } = await supabase.auth.getUser();
if (data.user) {
const { data: prof } = await supabase
.from('profiles')
.select('shop_id, role, name, fcm_token')
.eq('id', data.user.id)
.single();
setUser({
id: data.user.id,
name: prof?.name || data.user.email?.split('@')[0] || 'Utilizador',
email: data.user.email || '',
role: (prof?.role as any) || 'cliente',
shopId: prof?.shop_id || undefined,
fcmToken: prof?.fcm_token || undefined,
} as User);
// Regista token FCM se ainda não existir ou tiver mudado
registerForPushNotificationsAsync().then(token => {
if (token && token !== prof?.fcm_token) {
supabase.from('profiles').update({ fcm_token: token }).eq('id', data.user.id).then();
}
});
}
if (data.user) await applySupabaseUser(data.user);
};
loadUser();
}, []);
@@ -102,18 +116,32 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
if (shopsData) {
const merged: BarberShop[] = shopsData.map((shop: any) => ({
...shop,
id: shop.id,
name: shop.name,
address: shop.address || '',
rating: shop.rating || 0,
imageUrl: shop.image_url || shop.imageUrl || undefined,
schedule: shop.schedule || undefined,
paymentMethods: shop.payment_methods || undefined,
socialNetworks: shop.social_networks || undefined,
contacts: shop.contacts || undefined,
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,
price: s.price || 0,
duration: s.duration || 30,
barberIds: s.barber_ids || [],
})),
products: (productsData || []).filter((p: any) => p.shop_id === shop.id),
products: (productsData || []).filter((p: any) => p.shop_id === shop.id).map((p: any) => ({
id: p.id,
name: p.name,
price: p.price || 0,
stock: p.stock || 0,
})),
barbers: (barbersData || []).filter((b: any) => b.shop_id === shop.id).map((b: any) => ({
id: b.id,
name: b.name,
imageUrl: b.image_url || undefined,
specialties: b.specialties || [],
schedule: b.schedule || [],
})),
@@ -176,8 +204,10 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
}))
);
}
setShopsReady(true);
} catch (err) {
console.error('Error refreshing shops:', err);
setShopsReady(true);
}
};
@@ -197,18 +227,16 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
init();
}, []);
const login = (email: string, password: string) => {
// Provisório para demo
const u: User = {
id: email === 'barber@demo.com' ? 'demo-barber' : 'demo-cliente',
name: email === 'barber@demo.com' ? 'Barbeiro Chefe' : 'Utilizador Demo',
email,
password, // Adicionado para satisfazer o tipo
role: email === 'barber@demo.com' ? 'barbearia' : 'cliente',
shopId: email === 'barber@demo.com' ? shops[0]?.id : undefined
};
setUser(u);
return u;
const login = async (email: string, password: string) => {
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error || !data.user) {
console.error('Erro no login:', error);
return undefined;
}
const nextUser = await applySupabaseUser(data.user);
await refreshShops();
return nextUser;
};
const logout = async () => {
@@ -216,18 +244,43 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setUser(undefined);
};
const register = (payload: any) => {
const id = Math.random().toString(36).substring(2, 15);
const newUser: User = { ...payload, id };
setUser(newUser);
return newUser;
const register = async (payload: any) => {
const { name, email, password, role, shopName } = payload;
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name,
role,
shopName: role === 'barbearia' ? shopName : null,
shop_name: role === 'barbearia' ? shopName : null,
},
},
});
if (error) throw error;
if (!data.user) throw new Error('Erro ao criar conta');
if (!data.session) return undefined;
const nextUser = await applySupabaseUser(data.user);
await refreshShops();
return nextUser;
};
const toggleFavorite = (shopId: string) => {
setFavorites((prev) =>
prev.includes(shopId) ? prev.filter((id) => id !== shopId) : [...prev, shopId]
);
};
const isFavorite = (shopId: string) => favorites.includes(shopId);
const removeFromCart = (refId: string) => {
setCart((prev) => prev.filter((i) => i.refId !== refId));
};
const placeOrder = (customerId: string, shopId: string) => {
const placeOrder = async (customerId: string, shopId: string) => {
const shopItems = cart.filter((i) => i.shopId === shopId);
if (!shopItems.length) return null;
@@ -240,23 +293,42 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return sum + price * i.qty;
}, 0);
const newOrder: Order = {
id: Math.random().toString(36).substring(2, 15),
shopId,
customerId,
const { data, error } = await supabase.from('orders').insert([{
shop_id: shopId,
customer_id: customerId,
items: shopItems,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
}]).select().single();
if (error || !data) {
console.error('Erro ao criar pedido:', error);
return null;
}
const newOrder: Order = {
id: data.id,
shopId: data.shop_id,
customerId: data.customer_id,
items: data.items,
total: data.total,
status: data.status as Order['status'],
createdAt: data.created_at,
};
setOrders((prev) => [...prev, newOrder]);
await refreshShops();
setCart((prev) => prev.filter((i) => i.shopId !== shopId));
return newOrder;
};
const addToCart = (item: CartItem) => {
setCart((prev: CartItem[]) => [...prev, item]);
setCart((prev: CartItem[]) => {
const next = [...prev];
const existing = next.find((i) => i.shopId === item.shopId && i.type === item.type && i.refId === item.refId);
if (existing) existing.qty += item.qty;
else next.push(item);
return next;
});
};
const clearCart = () => setCart([]);
@@ -294,13 +366,23 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
};
const addBarber = async (shopId: string, barber: Omit<Barber, 'id'>) => {
await supabase.from('barbers').insert([{ shop_id: shopId, ...barber }]);
await supabase.from('barbers').insert([{
shop_id: shopId,
name: barber.name,
specialties: barber.specialties,
schedule: barber.schedule,
image_url: barber.imageUrl,
}]);
await refreshShops();
};
const updateBarber = async (shopId: string, barber: Barber) => {
const { id, ...data } = barber;
await supabase.from('barbers').update(data).eq('id', id);
await supabase.from('barbers').update({
name: barber.name,
specialties: barber.specialties,
schedule: barber.schedule,
image_url: barber.imageUrl,
}).eq('id', barber.id);
await refreshShops();
};
@@ -383,7 +465,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
() => ({
user,
shops,
shopsReady,
cart,
favorites,
appointments,
orders,
waitlists,
@@ -391,6 +475,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
login,
logout,
register,
toggleFavorite,
isFavorite,
addToCart,
removeFromCart,
placeOrder,
@@ -411,7 +497,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
markNotificationRead,
refreshShops,
}),
[user, shops, cart, appointments, orders, waitlists, notifications]
[user, shops, shopsReady, cart, favorites, appointments, orders, waitlists, notifications]
);
if (loading) return null;
@@ -427,6 +513,13 @@ export const useApp = () => {
async function registerForPushNotificationsAsync() {
let token;
if (Platform.OS === 'android' && Constants.appOwnership === 'expo') {
console.log('Push remoto Android requer development build; ignorado no Expo Go.');
return;
}
const Notifications = await import('expo-notifications');
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
@@ -455,4 +548,4 @@ async function registerForPushNotificationsAsync() {
}
return token;
}
}