feat: enhance booking flow with optional serviceId and improve dashboard settings

- Updated RootStackParamList to include optional serviceId in Booking route.
- Modified Booking page to initialize step and serviceId based on route parameters.
- Enhanced Dashboard page with new settings tab for shop details, including image upload and JSON schedule management.
- Added functionality to upload barber images and cover images.
- Improved Profile page with review submission for appointments and better notification handling.
- Updated ShopDetails to pass serviceId when navigating to Booking.
This commit is contained in:
2026-05-06 11:36:11 +01:00
parent a065130167
commit 99fc0a3882
8 changed files with 809 additions and 165 deletions

View File

@@ -7,6 +7,7 @@
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 { storage } from '../lib/storage';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
@@ -45,6 +46,8 @@ type AppContextValue = State & {
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => Promise<void>;
updateBarber: (shopId: string, barber: Barber) => Promise<void>;
deleteBarber: (shopId: string, barberId: string) => Promise<void>;
updateShopDetails: (shopId: string, payload: Partial<BarberShop>) => Promise<void>;
submitReview: (shopId: string, appointmentId: string, rating: number, comment: string) => Promise<void>;
joinWaitlist: (shopId: string, serviceId: string, barberId: string, date: string) => Promise<boolean>;
markNotificationRead: (id: string) => Promise<void>;
refreshShops: () => Promise<void>;
@@ -64,6 +67,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [shopsReady, setShopsReady] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
let mounted = true;
storage.get<{ favorites?: string[]; cart?: CartItem[] }>('smart-agenda-mobile-state', {}).then((stored) => {
if (!mounted) return;
if (Array.isArray(stored.favorites)) setFavorites(stored.favorites);
if (Array.isArray(stored.cart)) setCart(stored.cart);
});
return () => {
mounted = false;
};
}, []);
useEffect(() => {
storage.set('smart-agenda-mobile-state', { favorites, cart });
}, [favorites, cart]);
const applySupabaseUser = async (authUser: any): Promise<User | undefined> => {
if (!authUser) return undefined;
@@ -113,39 +132,47 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const { data: ordersData } = await supabase.from('orders').select('*');
const { data: waitlistsData } = await supabase.from('waitlist').select('*');
const { data: notificationsData } = await supabase.from('notifications').select('*');
const { data: reviewsData } = await supabase.from('reviews').select('shop_id, rating');
if (shopsData) {
const merged: BarberShop[] = shopsData.map((shop: any) => ({
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 || 0,
duration: s.duration || 30,
barberIds: s.barber_ids || [],
})),
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 || [],
})),
}));
const merged: BarberShop[] = shopsData.map((shop: any) => {
const shopReviews = (reviewsData || []).filter((r: any) => r.shop_id === shop.id);
const avgRating = shopReviews.length
? shopReviews.reduce((sum: number, r: any) => sum + (r.rating || 0), 0) / shopReviews.length
: shop.rating || 0;
return {
id: shop.id,
name: shop.name,
address: shop.address || '',
rating: avgRating,
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 || 0,
duration: s.duration || 30,
barberIds: s.barber_ids || [],
})),
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 || [],
})),
};
});
setShops(merged);
}
@@ -334,13 +361,23 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const clearCart = () => setCart([]);
const addService = async (shopId: string, service: Omit<Service, 'id'>) => {
await supabase.from('services').insert([{ shop_id: shopId, ...service }]);
await supabase.from('services').insert([{
shop_id: shopId,
name: service.name,
price: service.price,
duration: service.duration,
barber_ids: service.barberIds || [],
}]);
await refreshShops();
};
const updateService = async (shopId: string, service: Service) => {
const { id, ...data } = service;
await supabase.from('services').update(data).eq('id', id);
await supabase.from('services').update({
name: service.name,
price: service.price,
duration: service.duration,
barber_ids: service.barberIds || [],
}).eq('id', service.id);
await refreshShops();
};
@@ -391,6 +428,41 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
await refreshShops();
};
const updateShopDetails = async (shopId: string, payload: Partial<BarberShop>) => {
const { services, products, barbers, id, imageUrl, paymentMethods, socialNetworks, contacts, schedule, ...rest } = payload;
const updateData: Record<string, any> = { ...rest };
if (imageUrl !== undefined) updateData.image_url = imageUrl;
if (paymentMethods !== undefined) updateData.payment_methods = paymentMethods;
if (socialNetworks !== undefined) updateData.social_networks = socialNetworks;
if (contacts !== undefined) updateData.contacts = contacts;
if (schedule !== undefined) updateData.schedule = schedule;
const { error } = await supabase.from('shops').update(updateData).eq('id', shopId);
if (error) throw error;
if (payload.name) {
await supabase.from('profiles').update({ shop_name: payload.name }).eq('shop_id', shopId);
}
setShops((prev) => prev.map((shop) => (shop.id === shopId ? { ...shop, ...payload } : shop)));
};
const submitReview = async (shopId: string, appointmentId: string, rating: number, comment: string) => {
const { data } = await supabase.auth.getUser();
const customerId = data.user?.id;
if (!customerId) throw new Error('Sessão expirada. Faz login novamente.');
const { error } = await supabase.from('reviews').insert([{
shop_id: shopId,
customer_id: customerId,
appointment_id: appointmentId,
rating,
comment: comment.trim() || null,
}]);
if (error) throw error;
await refreshShops();
};
const createAppointment = async (input: Omit<Appointment, 'id' | 'status' | 'total'>) => {
const svc = shops.flatMap(s => s.services).find(s => s.id === input.serviceId);
const total = svc ? svc.price : 0;
@@ -493,6 +565,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
addBarber,
updateBarber,
deleteBarber,
updateShopDetails,
submitReview,
joinWaitlist,
markNotificationRead,
refreshShops,