feat: Implement initial application structure, core pages, UI components, and Supabase backend integration.

This commit is contained in:
Rodrigo Lopes dos Santos
2026-03-16 01:30:28 +00:00
parent 8ece90a37e
commit 0270a6cbdf
49 changed files with 2122 additions and 797 deletions

View File

@@ -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;
}