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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user