feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.
This commit is contained in:
@@ -8,6 +8,9 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from '
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as Device from 'expo-device';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
type State = {
|
||||
user?: User;
|
||||
@@ -18,10 +21,12 @@ type State = {
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => boolean;
|
||||
login: (email: string, password: string) => User | undefined;
|
||||
logout: () => void;
|
||||
register: (payload: any) => boolean;
|
||||
register: (payload: any) => User | undefined;
|
||||
addToCart: (item: CartItem) => void;
|
||||
removeFromCart: (refId: string) => void;
|
||||
placeOrder: (customerId: string, shopId: string) => Order | null;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
|
||||
@@ -54,7 +59,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (data.user) {
|
||||
const { data: prof } = await supabase
|
||||
.from('profiles')
|
||||
.select('shop_id, role, name')
|
||||
.select('shop_id, role, name, fcm_token')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
|
||||
@@ -64,13 +69,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const refreshShops = async () => {
|
||||
console.log('AppContext: refreshShops iniciado');
|
||||
try {
|
||||
const { data: shopsData } = await supabase.from('shops').select('*');
|
||||
const { data: servicesData } = await supabase.from('services').select('*');
|
||||
@@ -134,15 +148,33 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AppContext: Iniciando carregamento...');
|
||||
const init = async () => {
|
||||
await refreshShops();
|
||||
setLoading(false);
|
||||
try {
|
||||
await refreshShops();
|
||||
console.log('AppContext: Lojas carregadas com sucesso.');
|
||||
} catch (e) {
|
||||
console.error('AppContext: Erro no init:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.log('AppContext: setLoading(false)');
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const login = (email: string, password: string) => {
|
||||
return true;
|
||||
// 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 logout = async () => {
|
||||
@@ -154,7 +186,39 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const id = nanoid();
|
||||
const newUser: User = { ...payload, id };
|
||||
setUser(newUser);
|
||||
return true;
|
||||
return newUser;
|
||||
};
|
||||
|
||||
const removeFromCart = (refId: string) => {
|
||||
setCart((prev) => prev.filter((i) => i.refId !== refId));
|
||||
};
|
||||
|
||||
const placeOrder = (customerId: string, shopId: string) => {
|
||||
const shopItems = cart.filter((i) => i.shopId === shopId);
|
||||
if (!shopItems.length) return null;
|
||||
|
||||
const shop = shops.find((s) => s.id === shopId);
|
||||
const total = shopItems.reduce((sum, i) => {
|
||||
const price =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)?.price ?? 0
|
||||
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
|
||||
return sum + price * i.qty;
|
||||
}, 0);
|
||||
|
||||
const newOrder: Order = {
|
||||
id: nanoid(),
|
||||
shopId,
|
||||
customerId,
|
||||
items: shopItems,
|
||||
total,
|
||||
status: 'pendente',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setOrders((prev) => [...prev, newOrder]);
|
||||
setCart((prev) => prev.filter((i) => i.shopId !== shopId));
|
||||
return newOrder;
|
||||
};
|
||||
|
||||
const addToCart = (item: CartItem) => {
|
||||
@@ -221,7 +285,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total
|
||||
total,
|
||||
reminder_minutes: input.reminderMinutes
|
||||
}]).select().single();
|
||||
await refreshShops();
|
||||
return data as any as Appointment;
|
||||
@@ -248,6 +313,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
placeOrder,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
updateAppointmentStatus,
|
||||
@@ -275,4 +342,36 @@ export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
return ctx;
|
||||
};
|
||||
};
|
||||
|
||||
async function registerForPushNotificationsAsync() {
|
||||
let token;
|
||||
if (Platform.OS === 'android') {
|
||||
await Notifications.setNotificationChannelAsync('default', {
|
||||
name: 'default',
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
vibrationPattern: [0, 250, 250, 250],
|
||||
lightColor: '#FF231F7C',
|
||||
});
|
||||
}
|
||||
|
||||
if (Device.isDevice) {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
if (finalStatus !== 'granted') {
|
||||
console.log('Falha ao obter token push!');
|
||||
return;
|
||||
}
|
||||
token = (await Notifications.getExpoPushTokenAsync({
|
||||
projectId: 'b018a5db-c940-4364-81ee-596ced75cae3',
|
||||
})).data;
|
||||
} else {
|
||||
console.log('Necessário dispositivo físico para notificações push');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
Reference in New Issue
Block a user