This commit is contained in:
2026-02-25 09:59:12 +00:00
parent a895f83db7
commit c4164e50a0
12 changed files with 303 additions and 434 deletions

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { View, StyleSheet, ViewStyle } from 'react-native';
import { View, StyleSheet, ViewStyle, StyleProp } from 'react-native';
type Props = {
children: React.ReactNode;
style?: ViewStyle;
style?: StyleProp<ViewStyle>;
};
export const Card = ({ children, style }: Props) => {
@@ -25,4 +25,3 @@ const styles = StyleSheet.create({
},
});

View File

@@ -1,315 +1,169 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { nanoid } from 'nanoid';
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { mockShops, mockUsers } from '../data/mock';
import { storage } from '../lib/storage';
import { supabase } from '../lib/supabase';
type State = {
user?: User;
users: User[];
shops: BarberShop[];
appointments: Appointment[];
orders: Order[];
cart: CartItem[];
};
type AppContextValue = State & {
login: (email: string, password: string) => User | null;
logout: () => void;
register: (payload: Omit<User, 'id' | 'shopId'> & { shopName?: string }) => User | null;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
placeOrder: (customerId: string, shopId?: string) => Order | null;
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
updateOrderStatus: (id: string, status: Order['status']) => void;
addService: (shopId: string, service: Omit<Service, 'id'>) => void;
updateService: (shopId: string, service: Service) => void;
deleteService: (shopId: string, serviceId: string) => void;
addProduct: (shopId: string, product: Omit<Product, 'id'>) => void;
updateProduct: (shopId: string, product: Product) => void;
deleteProduct: (shopId: string, productId: string) => void;
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
updateBarber: (shopId: string, barber: Barber) => void;
deleteBarber: (shopId: string, barberId: string) => void;
};
const initialState: State = {
user: undefined,
users: mockUsers,
shops: mockShops,
appointments: [],
orders: [],
cart: [],
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
updateService: (shopId: string, service: Service) => Promise<void>;
deleteService: (shopId: string, serviceId: string) => Promise<void>;
refreshShops: () => Promise<void>;
};
const AppContext = createContext<AppContextValue | undefined>(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<State>(initialState);
const [isLoading, setIsLoading] = useState(true);
const [shops, setShops] = useState<BarberShop[]>([]);
const [cart, setCart] = useState<CartItem[]>([]);
const [user, setUser] = useState<User | undefined>(undefined);
const [loading, setLoading] = useState(true);
// 🔹 Carregar utilizador autenticado
useEffect(() => {
const loadData = async () => {
try {
const saved = await storage.get('smart-agenda', initialState);
setState(saved);
} catch (err) {
console.error('Error loading data:', err);
} finally {
setIsLoading(false);
const loadUser = async () => {
const { data } = await supabase.auth.getUser();
if (data.user) {
setUser({
id: data.user.id,
email: data.user.email || '',
role: 'barbearia', // ajustar se tiveres roles
} as User);
}
};
loadData();
loadUser();
}, []);
// 🔹 Buscar shops + services
const refreshShops = async () => {
console.log("A buscar shops...");
const { data: shopsData, error: shopsError } = await supabase
.from('shops')
.select('*');
if (shopsError) {
console.error("Erro ao buscar shops:", shopsError);
return;
}
const { data: servicesData, error: servicesError } = await supabase
.from('services')
.select('*');
if (servicesError) {
console.error("Erro ao buscar services:", servicesError);
return;
}
// Associar serviços às respetivas shops
const shopsWithServices = shopsData.map((shop) => ({
...shop,
services: servicesData.filter((s) => s.shop_id === shop.id),
products: [],
barbers: [],
}));
console.log("Shops carregadas:", shopsWithServices);
setShops(shopsWithServices);
};
useEffect(() => {
if (!isLoading) {
storage.set('smart-agenda', state);
}
}, [state, isLoading]);
const login = (email: string, password: string) => {
const found = state.users.find((u) => u.email === email && u.password === password);
if (found) {
setState((s) => ({ ...s, user: found }));
return found;
}
return null;
};
const logout = () => setState((s) => ({ ...s, user: undefined }));
const register: AppContextValue['register'] = ({ shopName, ...payload }) => {
const exists = state.users.some((u) => u.email === payload.email);
if (exists) return null;
if (payload.role === 'barbearia') {
const shopId = nanoid();
const shop: BarberShop = {
id: shopId,
name: shopName || `Barbearia ${payload.name}`,
address: 'Endereço a definir',
rating: 0,
barbers: [],
services: [],
products: [],
};
const user: User = { ...payload, id: nanoid(), role: 'barbearia', shopId };
setState((s) => ({
...s,
user,
users: [...s.users, user],
shops: [...s.shops, shop],
}));
return user;
}
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
setState((s) => ({
...s,
user,
users: [...s.users, user],
}));
return user;
};
const addToCart: AppContextValue['addToCart'] = (item) => {
setState((s) => {
const cart = [...s.cart];
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type && c.shopId === item.shopId);
if (idx >= 0) cart[idx].qty += item.qty;
else cart.push(item);
return { ...s, cart };
});
};
const removeFromCart: AppContextValue['removeFromCart'] = (refId) => {
setState((s) => ({ ...s, cart: s.cart.filter((c) => c.refId !== refId) }));
};
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
const createAppointment: AppContextValue['createAppointment'] = (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);
if (!svc) return null;
const exists = state.appointments.find(
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
);
if (exists) return null;
const appointment: Appointment = {
...input,
id: nanoid(),
status: 'pendente',
total: svc.price,
const init = async () => {
await refreshShops();
setLoading(false);
};
init();
}, []);
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
return appointment;
const logout = async () => {
await supabase.auth.signOut();
setUser(undefined);
};
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
if (!state.cart.length) return null;
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
const newOrders: Order[] = entries.map(([shopId, items]) => {
const total = items.reduce((sum, item) => {
const shop = state.shops.find((s) => s.id === item.shopId);
if (!shop) return sum;
const price =
item.type === 'service'
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
return sum + price * item.qty;
}, 0);
return {
id: nanoid(),
shopId,
customerId,
items,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
};
});
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
return newOrders[0] ?? null;
const addToCart = (item: CartItem) => {
setCart((prev) => [...prev, item]);
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
setState((s) => ({
...s,
appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
}));
const clearCart = () => setCart([]);
// 🔹 CRUD SERVICES (SUPABASE REAL)
const addService = async (shopId: string, service: Omit<Service, 'id'>) => {
const { error } = await supabase.from('services').insert([
{
shop_id: shopId,
name: service.name,
price: service.price,
duration: service.duration,
},
]);
if (error) {
console.error("Erro ao adicionar serviço:", error);
return;
}
await refreshShops();
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
setState((s) => ({
...s,
orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
}));
const updateService = async (shopId: string, service: Service) => {
const { error } = await supabase
.from('services')
.update({
name: service.name,
price: service.price,
duration: service.duration,
})
.eq('id', service.id);
if (error) {
console.error("Erro ao atualizar serviço:", error);
return;
}
await refreshShops();
};
const addService: AppContextValue['addService'] = (shopId, service) => {
const entry: Service = { ...service, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, services: [...shop.services, entry] } : shop)),
}));
};
const deleteService = async (shopId: string, serviceId: string) => {
const { error } = await supabase
.from('services')
.delete()
.eq('id', serviceId);
const updateService: AppContextValue['updateService'] = (shopId, service) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.map((sv) => (sv.id === service.id ? service : sv)) } : shop
),
}));
};
if (error) {
console.error("Erro ao apagar serviço:", error);
return;
}
const deleteService: AppContextValue['deleteService'] = (shopId, serviceId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.filter((sv) => sv.id !== serviceId) } : shop
),
}));
};
const addProduct: AppContextValue['addProduct'] = (shopId, product) => {
const entry: Product = { ...product, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, products: [...shop.products, entry] } : shop)),
}));
};
const updateProduct: AppContextValue['updateProduct'] = (shopId, product) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.map((p) => (p.id === product.id ? product : p)) } : shop
),
}));
};
const deleteProduct: AppContextValue['deleteProduct'] = (shopId, productId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.filter((p) => p.id !== productId) } : shop
),
}));
};
const addBarber: AppContextValue['addBarber'] = (shopId, barber) => {
const entry: Barber = { ...barber, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, barbers: [...shop.barbers, entry] } : shop)),
}));
};
const updateBarber: AppContextValue['updateBarber'] = (shopId, barber) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.map((b) => (b.id === barber.id ? barber : b)) } : shop
),
}));
};
const deleteBarber: AppContextValue['deleteBarber'] = (shopId, barberId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.filter((b) => b.id !== barberId) } : shop
),
}));
await refreshShops();
};
const value: AppContextValue = useMemo(
() => ({
...state,
login,
user,
shops,
cart,
logout,
register,
addToCart,
removeFromCart,
clearCart,
createAppointment,
placeOrder,
updateAppointmentStatus,
updateOrderStatus,
addService,
updateService,
deleteService,
addProduct,
updateProduct,
deleteProduct,
addBarber,
updateBarber,
deleteBarber,
refreshShops,
}),
[state]
[user, shops, cart]
);
if (isLoading) {
return null; // Ou um componente de loading
}
if (loading) return null;
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
@@ -318,5 +172,4 @@ export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
return ctx;
};
};

View File

@@ -1,14 +1,16 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
import { RootStackParamList } from '../navigation/types';
export default function Explore() {
const navigation = useNavigation();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { shops } = useApp();
return (
@@ -32,14 +34,14 @@ export default function Explore() {
</View>
<View style={styles.buttons}>
<Button
onPress={() => navigation.navigate('ShopDetails' as never, { shopId: shop.id } as never)}
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
variant="outline"
style={styles.button}
>
Ver detalhes
</Button>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.button}
>
Agendar
@@ -106,4 +108,3 @@ const styles = StyleSheet.create({
},
});

View File

@@ -1,16 +1,18 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
import { RootStackParamList } from '../navigation/types';
export default function ShopDetails() {
const route = useRoute();
const navigation = useNavigation();
const { shopId } = route.params as { shopId: string };
const route = useRoute<RouteProp<RootStackParamList, 'ShopDetails'>>();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { shopId } = route.params;
const { shops, addToCart } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
@@ -29,7 +31,7 @@ export default function ShopDetails() {
<Text style={styles.title}>{shop.name}</Text>
<Text style={styles.address}>{shop.address}</Text>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
onPress={() => navigation.navigate('Booking', { shopId: shop.id })}
style={styles.bookButton}
>
Agendar
@@ -181,4 +183,3 @@ const styles = StyleSheet.create({
},
});