diff --git a/src/components/ShopCard.tsx b/src/components/ShopCard.tsx new file mode 100644 index 0000000..5ea59f5 --- /dev/null +++ b/src/components/ShopCard.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { BarberShop } from '../types'; +import { colors, radius, shadows } from '../lib/theme'; + +type Props = { + shop: BarberShop; + isFavorite?: boolean; + onPress: () => void; + onToggleFavorite?: () => void; +}; + +export const ShopCard = ({ shop, isFavorite, onPress, onToggleFavorite }: Props) => ( + + + {shop.imageUrl ? ( + + ) : ( + + + + )} + + + + {(shop.rating || 0).toFixed(1)} + + {onToggleFavorite && ( + { + e.stopPropagation?.(); + onToggleFavorite(); + }} + > + + + )} + + + + {shop.name} + + + + {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Endereço Indisponível'} + + + + + +{(shop.barbers || []).length} Barbeiros + + Reservar + + + + +); + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.surface, + borderRadius: radius.xl, + overflow: 'hidden', + borderWidth: 1, + borderColor: colors.border, + ...shadows.card, + }, + imageWrap: { + height: 150, + position: 'relative', + }, + image: { + width: '100%', + height: '100%', + }, + imageFallback: { + width: '100%', + height: '100%', + backgroundColor: colors.surfaceDark, + alignItems: 'center', + justifyContent: 'center', + }, + imageOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(15,23,42,0.25)', + }, + ratingBadge: { + position: 'absolute', + top: 12, + right: 12, + flexDirection: 'row', + alignItems: 'center', + gap: 4, + backgroundColor: 'rgba(15,23,42,0.9)', + borderRadius: radius.pill, + paddingHorizontal: 10, + paddingVertical: 4, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.1)', + }, + ratingText: { + color: colors.textInverse, + fontSize: 11, + fontWeight: '800', + }, + favBtn: { + position: 'absolute', + top: 12, + left: 12, + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: 'rgba(15,23,42,0.55)', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.12)', + }, + body: { + padding: 16, + gap: 8, + }, + name: { + fontSize: 18, + fontWeight: '900', + color: colors.text, + letterSpacing: -0.3, + }, + addressRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + address: { + flex: 1, + color: colors.textMuted, + fontSize: 13, + fontWeight: '500', + }, + footer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 4, + gap: 8, + }, + barbersLabel: { + flex: 1, + fontSize: 10, + fontWeight: '800', + color: colors.textSubtle, + textTransform: 'uppercase', + letterSpacing: 0.8, + }, + cta: { + paddingHorizontal: 14, + paddingVertical: 8, + backgroundColor: colors.surfaceDark, + borderRadius: radius.md, + minHeight: 36, + alignItems: 'center', + justifyContent: 'center', + }, + ctaText: { + color: colors.brandAccent, + fontSize: 12, + fontWeight: '800', + }, +}); diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 9df35b0..d9a5e84 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -11,15 +11,15 @@ type Props = { const colorMap = { solid: { - amber: { bg: colors.accent, text: colors.textInverse }, - slate: { bg: colors.textSoft, text: colors.textInverse }, + amber: { bg: colors.primary, text: colors.textInverse }, + slate: { bg: colors.surfaceDark, text: colors.textInverse }, green: { bg: colors.success, text: colors.textInverse }, red: { bg: colors.danger, text: colors.textInverse }, blue: { bg: colors.primary, text: colors.textInverse }, indigo: { bg: colors.primary, text: colors.textInverse }, }, soft: { - amber: { bg: colors.accentSoft, text: '#7a4310' }, + amber: { bg: colors.primarySoft, text: colors.primaryDark }, slate: { bg: colors.backgroundAlt, text: colors.textSoft }, green: { bg: colors.successSoft, text: '#166534' }, red: { bg: colors.dangerSoft, text: '#991b1b' }, @@ -29,11 +29,11 @@ const colorMap = { }; export const Badge = ({ children, color = 'indigo', variant = 'soft', style }: Props) => { - const colors = colorMap[variant][color]; + const palette = colorMap[variant][color]; return ( - - {children} + + {children} ); }; @@ -50,4 +50,3 @@ const styles = StyleSheet.create({ fontWeight: '600', }, }); - diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 00ebd11..349132c 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -5,7 +5,7 @@ import { colors, radius, shadows } from '../../lib/theme'; type Props = { children: React.ReactNode; onPress?: () => void; - variant?: 'solid' | 'outline' | 'ghost'; + variant?: 'solid' | 'outline' | 'ghost' | 'dark'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; loading?: boolean; @@ -29,6 +29,11 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa textStyle, ]; + const spinnerColor = + variant === 'solid' ? colors.textInverse : + variant === 'dark' ? colors.brandAccent : + colors.primaryDark; + return ( {loading ? ( - + ) : ( {children} )} @@ -57,10 +62,14 @@ const styles = StyleSheet.create({ backgroundColor: colors.primary, ...shadows.soft, }, + dark: { + backgroundColor: colors.surfaceDark, + ...shadows.soft, + }, outline: { - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: colors.borderStrong, + backgroundColor: colors.surface, + borderWidth: 2, + borderColor: colors.primary, }, ghost: { backgroundColor: 'transparent', @@ -86,6 +95,9 @@ const styles = StyleSheet.create({ text_solid: { color: colors.textInverse, }, + text_dark: { + color: colors.brandAccent, + }, text_outline: { color: colors.primaryDark, }, diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 72195b2..ecb8969 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -14,7 +14,7 @@ export const Card = ({ children, style }: Props) => { const styles = StyleSheet.create({ card: { backgroundColor: colors.surface, - borderRadius: radius.sm, + borderRadius: radius.lg, padding: 16, borderWidth: 1, borderColor: colors.border, diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index f9ecdf1..04cd246 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -14,6 +14,7 @@ import { Platform, Alert } from 'react-native'; type State = { user?: User; + users: User[]; shops: BarberShop[]; shopsReady: boolean; cart: CartItem[]; @@ -64,6 +65,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [waitlists, setWaitlists] = useState([]); const [notifications, setNotifications] = useState([]); const [user, setUser] = useState(undefined); + const [users, setUsers] = useState([]); const [shopsReady, setShopsReady] = useState(false); const [loading, setLoading] = useState(true); @@ -166,11 +168,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; useEffect(() => { - const loadUser = async () => { - const { data } = await supabase.auth.getUser(); - if (data.user) await applySupabaseUser(data.user); + let mounted = true; + + const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => { + if (!mounted) return; + if (session?.user) { + applySupabaseUser(session.user).then(() => refreshShops()); + } else { + setUser(undefined); + } + setLoading(false); + }); + + return () => { + mounted = false; + sub.subscription.unsubscribe(); }; - loadUser(); }, []); const refreshShops = async () => { @@ -185,6 +198,20 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { 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'); + const { data: profilesData } = await supabase.from('profiles').select('id, name, role, shop_id'); + + if (profilesData) { + setUsers( + profilesData.map((p: any) => ({ + id: p.id, + name: p.name || 'Utilizador', + email: '', + password: '', + role: p.role === 'barbearia' ? 'barbearia' : 'cliente', + shopId: p.shop_id || undefined, + })) + ); + } if (shopsData) { const merged: BarberShop[] = shopsData.map((shop: any) => { @@ -203,6 +230,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { paymentMethods: shop.payment_methods || undefined, socialNetworks: shop.social_networks || undefined, contacts: shop.contacts || undefined, + ownerId: shop.owner_id || shop.ownerId || undefined, services: (servicesData || []).filter((s: any) => s.shop_id === shop.id).map((s: any) => ({ id: s.id, name: s.name, @@ -395,6 +423,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { createdAt: data.created_at, }; + const shopOwnerId = shop?.ownerId || shop?.id; + if (shopOwnerId) { + const clientName = user?.name || 'Um cliente'; + await supabase.from('notifications').insert([ + { user_id: shopOwnerId, message: `${clientName} fez um novo pedido de produtos!` }, + ]); + } + await refreshShops(); setCart((prev) => prev.filter((i) => i.shopId !== shopId)); return newOrder; @@ -562,20 +598,51 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }; const createAppointment = async (input: Omit) => { - const svc = shops.flatMap(s => s.services).find(s => s.id === input.serviceId); - const total = svc ? svc.price : 0; - const { data } = await supabase.from('appointments').insert([{ - shop_id: input.shopId, + const shopRef = shops.find((s) => s.id === input.shopId); + if (!shopRef) return null; + const svc = shopRef.services.find((s) => s.id === input.serviceId); + if (!svc) return null; + + const exists = appointments.find( + (ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado' + ); + if (exists) return null; + + const total = svc.price; + const { data, error } = await supabase.from('appointments').insert([{ + shop_id: input.shopId, service_id: input.serviceId, barber_id: input.barberId, customer_id: input.customerId, date: input.date, status: 'pendente', total, - reminder_minutes: input.reminderMinutes + reminder_minutes: input.reminderMinutes, }]).select().single(); + + if (error || !data) { + console.error('Erro ao criar marcação:', error); + return null; + } + + const shopOwnerId = shopRef.ownerId || shopRef.id; + const clientName = user?.name || 'Um cliente'; + await supabase.from('notifications').insert([ + { user_id: shopOwnerId, message: `${clientName} solicitou um novo agendamento para o dia ${input.date}!` }, + ]); + await refreshShops(); - return data as any as Appointment; + return { + id: data.id, + shopId: data.shop_id, + serviceId: data.service_id, + barberId: data.barber_id, + customerId: data.customer_id, + date: data.date, + status: data.status as Appointment['status'], + total: data.total, + reminderMinutes: data.reminder_minutes, + }; }; const updateAppointmentStatus = async (id: string, status: Appointment['status']) => { @@ -655,6 +722,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const value: AppContextValue = useMemo( () => ({ user, + users, shops, shopsReady, cart, @@ -690,7 +758,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { markNotificationRead, refreshShops, }), - [user, shops, shopsReady, cart, favorites, appointments, orders, waitlists, notifications] + [user, users, shops, shopsReady, cart, favorites, appointments, orders, waitlists, notifications] ); if (loading) return null; diff --git a/src/lib/theme.ts b/src/lib/theme.ts index c7b7606..7352691 100644 --- a/src/lib/theme.ts +++ b/src/lib/theme.ts @@ -1,42 +1,47 @@ import { Platform } from 'react-native'; +/** Paleta alinhada com web/src/index.css e tailwind (indigo → amber) */ export const colors = { - background: '#eef3ef', - backgroundAlt: '#e5ebe7', + background: '#f8fafc', + backgroundAlt: '#f1f5f9', surface: '#ffffff', - surfaceMuted: '#f7faf8', - surfaceDark: '#14211d', - surfaceDarkMuted: '#20312b', - text: '#17211d', - textSoft: '#35443e', - textMuted: '#64736d', - textSubtle: '#87948f', - textInverse: '#f8fbf8', - border: '#dce5df', - borderStrong: '#c8d5ce', + surfaceMuted: '#f1f5f9', + surfaceDark: '#0f172a', + surfaceDarkMuted: '#1e293b', + slate950: '#020617', + text: '#0f172a', + textSoft: '#334155', + textMuted: '#64748b', + textSubtle: '#94a3b8', + textInverse: '#ffffff', + border: '#e2e8f0', + borderStrong: '#cbd5e1', borderDark: 'rgba(255,255,255,0.12)', - primary: '#0f766e', - primaryDark: '#0b4f49', - primarySoft: '#d9f1ed', - accent: '#c87918', - accentSoft: '#fff1d1', + primary: '#f59e0b', + primaryDark: '#b45309', + primaryLight: '#fcd34d', + primarySoft: '#fef3c7', + brandAccent: '#fbbf24', + accent: '#f59e0b', + accentSoft: '#fef3c7', success: '#16a34a', successSoft: '#dcfce7', - danger: '#dc2626', - dangerSoft: '#fee2e2', + danger: '#e11d48', + dangerSoft: '#fff1f2', warning: '#d97706', warningSoft: '#fef3c7', - star: '#f2a51a', - overlay: 'rgba(12,20,17,0.56)', - placeholder: '#8a9791', + star: '#f59e0b', + overlay: 'rgba(15,23,42,0.56)', + placeholder: '#94a3b8', }; export const radius = { xs: 6, sm: 8, - md: 10, - lg: 12, - xl: 16, + md: 12, + lg: 16, + xl: 24, + xxl: 32, pill: 999, }; @@ -46,7 +51,7 @@ export const spacing = { const cardShadow = Platform.select({ ios: { - shadowColor: '#102018', + shadowColor: '#0f172a', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, shadowRadius: 18, @@ -55,7 +60,7 @@ const cardShadow = Platform.select({ elevation: 3, }, default: { - shadowColor: '#102018', + shadowColor: '#0f172a', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, shadowRadius: 18, @@ -65,7 +70,7 @@ const cardShadow = Platform.select({ const softShadow = Platform.select({ ios: { - shadowColor: '#102018', + shadowColor: '#0f172a', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12, @@ -74,7 +79,7 @@ const softShadow = Platform.select({ elevation: 2, }, default: { - shadowColor: '#102018', + shadowColor: '#0f172a', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.06, shadowRadius: 12, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 462587c..939ccb8 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -24,7 +24,7 @@ const TabIcon = ({ name, focused }: { name: any; focused: boolean }) => ( ); @@ -67,8 +67,8 @@ function ClientTabs() { screenOptions={{ headerShown: false, tabBarStyle: tabStyles.bar, - tabBarActiveTintColor: colors.textInverse, - tabBarInactiveTintColor: '#9cad9f', + tabBarActiveTintColor: colors.brandAccent, + tabBarInactiveTintColor: colors.textSubtle, tabBarLabelStyle: tabStyles.label, }} > @@ -128,8 +128,8 @@ function BarbeariaTabs() { screenOptions={{ headerShown: false, tabBarStyle: tabStyles.bar, - tabBarActiveTintColor: colors.textInverse, - tabBarInactiveTintColor: '#9cad9f', + tabBarActiveTintColor: colors.brandAccent, + tabBarInactiveTintColor: colors.textSubtle, tabBarLabelStyle: tabStyles.label, }} > @@ -216,10 +216,10 @@ const iconStyles = StyleSheet.create({ justifyContent: 'center', }, wrapActive: { - backgroundColor: colors.primary, + backgroundColor: colors.surfaceDarkMuted, }, emoji: { fontSize: 18, - color: '#9cad9f', + color: colors.textSubtle, }, }); diff --git a/src/pages/AuthLogin.tsx b/src/pages/AuthLogin.tsx index b434e28..c298cbe 100644 --- a/src/pages/AuthLogin.tsx +++ b/src/pages/AuthLogin.tsx @@ -76,7 +76,7 @@ export default function AuthLogin() { placeholder="••••••••" /> - @@ -110,11 +110,10 @@ const styles = StyleSheet.create({ logoRow: { alignItems: 'center', marginBottom: 28, - backgroundColor: colors.surface, - borderRadius: radius.sm, - borderWidth: 1, - borderColor: colors.border, + backgroundColor: colors.surfaceDark, + borderRadius: radius.lg, paddingVertical: 18, + paddingHorizontal: 24, ...shadows.card, }, logoImage: { @@ -151,7 +150,6 @@ const styles = StyleSheet.create({ submitButton: { width: '100%', marginTop: 4, - backgroundColor: colors.primary, paddingVertical: 16, }, guestButton: { diff --git a/src/pages/AuthRegister.tsx b/src/pages/AuthRegister.tsx index 855cfec..5effc51 100644 --- a/src/pages/AuthRegister.tsx +++ b/src/pages/AuthRegister.tsx @@ -107,7 +107,7 @@ export default function AuthRegister() { /> )} - diff --git a/src/pages/Booking.tsx b/src/pages/Booking.tsx index 55425b1..4bc5ce9 100644 --- a/src/pages/Booking.tsx +++ b/src/pages/Booking.tsx @@ -12,7 +12,7 @@ export default function Booking() { const route = useRoute(); const navigation = useNavigation(); const { shopId, serviceId: initialServiceId } = route.params as { shopId: string; serviceId?: string }; - const { shops, createAppointment, user, appointments, joinWaitlist } = useApp(); + const { shops, shopsReady, createAppointment, user, appointments, waitlists, joinWaitlist } = useApp(); const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]); @@ -42,15 +42,69 @@ export default function Booking() { return dates; }, []); + const generateDefaultSlots = (): string[] => { + const slots: string[] = []; + for (let hour = 9; hour <= 18; hour += 1) { + slots.push(`${hour.toString().padStart(2, '0')}:00`); + if (hour < 18) slots.push(`${hour.toString().padStart(2, '0')}:30`); + } + return slots; + }; + + const isShopClosedOnDate = useMemo(() => { + if (!date || !shop) return false; + const dateObj = new Date(`${date}T00:00:00`); + const ptDays = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado']; + const dayName = ptDays[dateObj.getDay()]; + + const exception = shop.schedule?.find((s) => s.isException && s.date === date); + if (exception) return Boolean(exception.closed); + + const defaultDay = shop.schedule?.find((s) => !s.isException && (s.day === dayName || s.day.startsWith(dayName.slice(0, 3)))); + if (defaultDay) return Boolean(defaultDay.closed); + + return dayName === 'Domingo'; + }, [shop?.schedule, date]); + const processedSlots = useMemo(() => { - if (!selectedBarber || !date) return []; - const slots = ["09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00"]; - const booked = appointments - .filter(a => a.barberId === barberId && a.status !== 'cancelado' && a.date.startsWith(date)) - .map(a => a.date.split(' ')[1]); - - return slots.map(t => ({ time: t, isBooked: booked.includes(t) })); - }, [selectedBarber, date, barberId, appointments]); + if (!selectedBarber || !date || isShopClosedOnDate) return []; + + const specificSchedule = selectedBarber.schedule?.find((s) => s.day === date); + const slots = + specificSchedule && specificSchedule.slots.length > 0 + ? [...specificSchedule.slots] + : generateDefaultSlots(); + + const bookedSlots = appointments + .filter((apt) => apt.barberId === barberId && apt.status !== 'cancelado' && apt.date.startsWith(date)) + .map((apt) => apt.date.split(' ')[1]) + .filter(Boolean); + + const todayStr = new Date().toISOString().split('T')[0]; + const isToday = date === todayStr; + const now = new Date(); + + return slots.map((time) => { + let isPast = false; + if (isToday) { + const [h, m] = time.split(':').map(Number); + if (h < now.getHours() || (h === now.getHours() && m <= now.getMinutes())) { + isPast = true; + } + } + const isBooked = bookedSlots.includes(time) || isPast; + const waitlistedByMe = user + ? waitlists.some( + (w) => + w.barberId === barberId && + w.date.startsWith(date) && + w.customerId === user.id && + w.status === 'pending' + ) + : false; + return { time, isBooked, waitlistedByMe }; + }); + }, [selectedBarber, date, barberId, appointments, user, waitlists, isShopClosedOnDate]); const submit = async () => { if (!user) { navigation.navigate('Login' as never); return; } @@ -65,9 +119,19 @@ export default function Booking() { if (appt) { Alert.alert('Sucesso', 'O seu agendamento foi confirmado.'); navigation.navigate('ExploreTab' as never); + } else { + Alert.alert('Erro', 'Não foi possível criar a marcação. O horário pode já estar ocupado.'); } }; + if (!shopsReady && !shop) { + return ( + + A preparar reserva... + + ); + } + if (!shop) return null; return ( @@ -171,20 +235,51 @@ export default function Booking() { Horários disponíveis - - {processedSlots.map(s => ( - setSlot(s.time)} - style={[styles.slotItem, slot === s.time && styles.slotActive, s.isBooked && styles.slotBooked]} - > - - {s.time} - - - ))} - + {isShopClosedOnDate ? ( + + O estabelecimento está fechado neste dia. + Selecione outra data. + + ) : processedSlots.some((s) => !s.isBooked) ? ( + + {processedSlots.filter((s) => !s.isBooked).map((s) => ( + setSlot(s.time)} + style={[styles.slotItem, slot === s.time && styles.slotActive]} + > + {s.time} + + ))} + + ) : processedSlots.length > 0 ? ( + + Todos os horários estão preenchidos. + {processedSlots.some((s) => s.waitlistedByMe) ? ( + Já estás na lista de espera deste dia. + ) : ( + + )} + + ) : ( + Sem horários para este dia. + )} {slot !== '' && ( @@ -231,6 +326,33 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, + centered: { + alignItems: 'center', + justifyContent: 'center', + }, + loadingText: { + color: colors.textMuted, + fontWeight: '700', + }, + closedBox: { + backgroundColor: colors.dangerSoft, + borderRadius: radius.lg, + borderWidth: 1, + borderColor: '#fecaca', + padding: 20, + gap: 10, + alignItems: 'center', + }, + closedTitle: { + color: colors.danger, + fontWeight: '800', + textAlign: 'center', + }, + closedHint: { + color: colors.textMuted, + fontSize: 13, + textAlign: 'center', + }, header: { flexDirection: 'row', alignItems: 'center', diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 2e9bd96..1335ae6 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -6,11 +6,34 @@ import * as ImagePicker from 'expo-image-picker'; 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 { supabase } from '../lib/supabase'; import { colors, radius, shadows } from '../lib/theme'; +import { ShopSchedule } from '../types'; -type Tab = 'agenda' | 'servicos' | 'produtos' | 'equipa' | 'perfil'; +type Tab = 'visao' | 'estatisticas' | 'agenda' | 'historico' | 'pedidos' | 'servicos' | 'produtos' | 'equipa' | 'perfil'; +type Period = 'hoje' | 'semana' | 'mes' | 'total'; + +const periods: Record boolean> = { + hoje: (d) => d.toDateString() === new Date().toDateString(), + semana: (d) => { + const now = new Date(); + return now.getTime() - d.getTime() <= 7 * 24 * 60 * 60 * 1000; + }, + mes: (d) => { + const now = new Date(); + return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear(); + }, + total: () => true, +}; + +const parseDate = (value: string) => new Date(value.replace(' ', 'T')); + +const filterSchedule = (sched: ShopSchedule[]) => { + const todayStr = new Date().toISOString().split('T')[0]; + return sched.filter((s) => !(s.isException && s.date && s.date < todayStr)); +}; const statusColor: Record = { pendente: colors.primary, @@ -32,8 +55,11 @@ export default function Dashboard() { user, shops, appointments, + orders, refreshShops, updateAppointmentStatus, + updateOrderStatus, + users, addService, deleteService, addProduct, @@ -45,7 +71,8 @@ export default function Dashboard() { } = useApp(); const shop = useMemo(() => shops.find((s) => s.id === user?.shopId), [shops, user?.shopId]); - const [activeTab, setActiveTab] = useState('agenda'); + const [activeTab, setActiveTab] = useState('visao'); + const [period, setPeriod] = useState('semana'); const [loading, setLoading] = useState(false); // Clear forms on tab change @@ -55,13 +82,51 @@ export default function Dashboard() { setFormBarb({ name: '', specialties: '' }); }, [activeTab]); - // Agenda Filters - const [dateFilter, setDateFilter] = useState(new Date().toISOString().split('T')[0]); - const shopAppointments = useMemo(() => { + const allShopAppointments = useMemo(() => { + if (!shop) return []; return appointments - .filter((a) => a.shopId === shop?.id && a.date.startsWith(dateFilter)) + .filter((a) => a.shopId === shop.id && periods[period](parseDate(a.date))) .sort((a, b) => a.date.localeCompare(b.date)); - }, [appointments, shop?.id, dateFilter]); + }, [appointments, shop?.id, period]); + + const activeShopAppointments = useMemo( + () => allShopAppointments.filter((a) => a.status !== 'concluido' && a.status !== 'cancelado'), + [allShopAppointments] + ); + + const pendingAppts = useMemo( + () => activeShopAppointments.filter((a) => a.status === 'pendente'), + [activeShopAppointments] + ); + + const confirmedAppts = useMemo( + () => activeShopAppointments.filter((a) => a.status === 'confirmado'), + [activeShopAppointments] + ); + + const historyAppointments = useMemo( + () => allShopAppointments.filter((a) => a.status === 'concluido' || a.status === 'cancelado'), + [allShopAppointments] + ); + + const shopOrders = useMemo(() => { + if (!shop) return []; + return orders.filter( + (o) => o.shopId === shop.id && o.status !== 'concluido' && periods[period](new Date(o.createdAt)) + ); + }, [orders, shop?.id, period]); + + const upcomingAppointments = useMemo( + () => + appointments + .filter((a) => { + if (a.shopId !== shop?.id || a.status === 'cancelado') return false; + return parseDate(a.date) > new Date(); + }) + .sort((a, b) => a.date.localeCompare(b.date)) + .slice(0, 5), + [appointments, shop?.id] + ); // Form states const [formSvc, setFormSvc] = useState({ name: '', price: '', duration: '' }); @@ -76,7 +141,9 @@ export default function Dashboard() { const [instagram, setInstagram] = useState(shop?.socialNetworks?.instagram || ''); const [facebook, setFacebook] = useState(shop?.socialNetworks?.facebook || ''); const [paymentMethods, setPaymentMethods] = useState(shop?.paymentMethods?.join(', ') || ''); - const [schedule, setSchedule] = useState(shop?.schedule || [ + const [exceptionDate, setExceptionDate] = useState(''); + const [exceptionClosed, setExceptionClosed] = useState(true); + const [schedule, setSchedule] = useState(shop?.schedule || [ { day: 'Segunda', open: '09:00', close: '19:00' }, { day: 'Terça', open: '09:00', close: '19:00' }, { day: 'Quarta', open: '09:00', close: '19:00' }, @@ -86,6 +153,121 @@ export default function Dashboard() { { day: 'Domingo', open: '00:00', close: '00:00', closed: true }, ]); + React.useEffect(() => { + if (shop?.schedule?.length) { + setSchedule(filterSchedule(shop.schedule)); + } + }, [shop?.id, shop?.schedule]); + + const weeklySchedule = useMemo(() => schedule.filter((s) => !s.isException), [schedule]); + const exceptionSlots = useMemo(() => schedule.filter((s) => s.isException), [schedule]); + + const statsData = useMemo(() => { + if (!shop) return null; + + const shopAppts = appointments.filter((a) => a.shopId === shop.id); + const totalConcluidas = shopAppts.filter((a) => a.status === 'concluido').length; + const totalCanceladas = shopAppts.filter((a) => a.status === 'cancelado').length; + const taxaConclusao = shopAppts.length > 0 ? Math.round((totalConcluidas / shopAppts.length) * 100) : 0; + const revenueServicos = shopAppts + .filter((a) => a.status === 'concluido') + .reduce((sum, a) => sum + (a.total || 0), 0); + const revenueProdutos = orders + .filter((o) => o.shopId === shop.id && o.status === 'concluido') + .reduce((sum, o) => sum + o.total, 0); + + const diasPT = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']; + const byDayOfWeek = Array.from({ length: 7 }, (_, i) => ({ name: diasPT[i], reservas: 0 })); + shopAppts.filter((a) => a.status !== 'cancelado').forEach((a) => { + const d = new Date(a.date.replace(' ', 'T')).getDay(); + byDayOfWeek[d].reservas += 1; + }); + + const svcMap = new Map(); + shopAppts.filter((a) => a.status !== 'cancelado').forEach((a) => { + const svc = shop.services.find((s) => s.id === a.serviceId); + if (!svc) return; + const prev = svcMap.get(a.serviceId)?.value ?? 0; + svcMap.set(a.serviceId, { name: svc.name, value: prev + 1 }); + }); + const topServices = Array.from(svcMap.values()).sort((a, b) => b.value - a.value).slice(0, 5); + + const barberMap = new Map(); + shopAppts.filter((a) => a.status !== 'cancelado').forEach((a) => { + const barber = shop.barbers.find((b) => b.id === a.barberId); + if (!barber) return; + const prev = barberMap.get(a.barberId)?.qty ?? 0; + barberMap.set(a.barberId, { name: barber.name, qty: prev + 1 }); + }); + const topBarbers = Array.from(barberMap.values()).sort((a, b) => b.qty - a.qty); + + const weeklyData: { name: string; reservas: number; concluidas: number }[] = []; + for (let i = 7; i >= 0; i -= 1) { + const wStart = new Date(); + wStart.setDate(wStart.getDate() - i * 7 - 6); + wStart.setHours(0, 0, 0, 0); + const wEnd = new Date(); + wEnd.setDate(wEnd.getDate() - i * 7); + wEnd.setHours(23, 59, 59, 999); + const label = wStart.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' }); + const weekAppts = shopAppts.filter((a) => { + const d = new Date(a.date.replace(' ', 'T')); + return d >= wStart && d <= wEnd; + }); + weeklyData.push({ + name: label, + reservas: weekAppts.length, + concluidas: weekAppts.filter((a) => a.status === 'concluido').length, + }); + } + + return { + totalAppts: shopAppts.length, + totalConcluidas, + totalCanceladas, + taxaConclusao, + revenueServicos, + revenueProdutos, + byDayOfWeek, + topServices, + topBarbers, + weeklyData, + }; + }, [appointments, orders, shop]); + + const updateWeeklySlot = (day: string, patch: Partial) => { + setSchedule((prev) => + prev.map((slot) => (slot.day === day && !slot.isException ? { ...slot, ...patch } : slot)) + ); + }; + + const addException = () => { + if (!exceptionDate.trim()) { + Alert.alert('Atenção', 'Selecione uma data para a exceção.'); + return; + } + if (schedule.some((s) => s.isException && s.date === exceptionDate)) { + Alert.alert('Atenção', 'Já existe uma exceção configurada para esta data.'); + return; + } + setSchedule((prev) => [ + ...prev, + { + day: 'Exceção', + open: '09:00', + close: '19:30', + closed: exceptionClosed, + date: exceptionDate.trim(), + isException: true, + }, + ]); + setExceptionDate(''); + }; + + const removeException = (date?: string) => { + setSchedule((prev) => prev.filter((s) => !(s.isException && s.date === date))); + }; + const pickImage = async (target: 'shop' | 'barber', barberId?: string) => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, @@ -160,6 +342,73 @@ export default function Dashboard() { if (!shop) return null; + const renderAppointmentActions = (appt: typeof appointments[number], mode: 'pending' | 'confirmed') => ( + + {mode === 'pending' ? ( + <> + updateAppointmentStatus(appt.id, 'cancelado')} + style={[styles.premiumActionBtn, styles.rejectBtn]} + > + Recusar + + updateAppointmentStatus(appt.id, 'confirmado')} + style={[styles.premiumActionBtn, styles.acceptBtn]} + > + Aceitar + + + ) : ( + <> + updateAppointmentStatus(appt.id, 'cancelado')} + style={[styles.premiumActionBtn, styles.rejectBtn]} + > + Cancelar + + updateAppointmentStatus(appt.id, 'concluido')} + style={[styles.premiumActionBtn, styles.completeBtn]} + > + Concluir + + + )} + + ); + + const renderAppointmentCard = (appt: typeof appointments[number], mode: 'pending' | 'confirmed' | 'history') => { + const svc = shop.services.find((s) => s.id === appt.serviceId); + const barb = shop.barbers.find((b) => b.id === appt.barberId); + const customer = users.find((u) => u.id === appt.customerId); + const aptDate = parseDate(appt.date); + + return ( + + + + {svc?.name || 'Serviço'} + {currency(appt.total)} + + Cliente: {customer?.name || 'Cliente'} · {barb?.name || 'Profissional'} + + {aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} às{' '} + {aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })} + + {mode === 'history' && ( + + + {statusLabel[appt.status]} + + + )} + {mode !== 'history' && renderAppointmentActions(appt, mode)} + + + ); + }; + return ( {/* Header Dashboard */} @@ -173,14 +422,35 @@ export default function Dashboard() { + + {([ + ['hoje', 'Hoje'], + ['semana', 'Semana'], + ['mes', 'Mês'], + ['total', 'Total'], + ] as const).map(([id, label]) => ( + setPeriod(id)} + style={[styles.periodChip, period === id && styles.periodChipActive]} + > + {label} + + ))} + + {/* Tabs Horizontais */} {[ - ['agenda', 'Agenda'], + ['visao', 'Visão Geral'], + ['estatisticas', 'Estatísticas'], + ['agenda', `Agendamentos${pendingAppts.length ? ` (${pendingAppts.length})` : ''}`], + ['historico', 'Histórico'], + ['pedidos', `Pedidos${shopOrders.length ? ` (${shopOrders.length})` : ''}`], ['servicos', 'Serviços'], ['produtos', 'Produtos'], - ['equipa', 'Equipa'], + ['equipa', 'Profissionais'], ['perfil', 'Definições'], ].map(([id, label]) => ( + {activeTab === 'visao' && ( + + + + {new Date().toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })} + + + {(() => { + const hour = new Date().getHours(); + if (hour < 12) return 'Bom dia'; + if (hour < 18) return 'Boa tarde'; + return 'Boa noite'; + })()}, {user?.name} + + + + + {[ + { label: 'Total de Reservas', value: String(allShopAppointments.length) }, + { label: 'Concluídas', value: String(allShopAppointments.filter((a) => a.status === 'concluido').length) }, + { label: 'Pendentes', value: String(pendingAppts.length) }, + { label: 'Clientes Únicos', value: String(new Set(allShopAppointments.map((a) => a.customerId)).size) }, + ].map((kpi) => ( + + {kpi.label} + {kpi.value} + + ))} + + + + + Próximos Agendamentos + setActiveTab('agenda')}> + Gerir → + + + {upcomingAppointments.length > 0 ? ( + upcomingAppointments.map((appt) => { + const svc = shop.services.find((s) => s.id === appt.serviceId); + const aptDate = parseDate(appt.date); + return ( + + {svc?.name || 'Serviço'} + + {aptDate.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} · {aptDate.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit' })} + + + ); + }) + ) : ( + Sem agendamentos futuros. + )} + + + + Resumo do Período + + Confirmados + {confirmedAppts.length} + + + Pedidos produtos + {shopOrders.length} + + + Serviços activos + {shop.services.length} + + + + )} + {activeTab === 'agenda' && ( - - - Marcações de Hoje - {shopAppointments.length} - - - - {shop.services.length} - Serviços - - - - {shop.products.reduce((sum, p) => sum + p.stock, 0)} - Stock - - - - ★ {shop.rating.toFixed(1)} - Avaliação - - + + Caixa de Pedidos + + Aprove ou recuse pedidos pendentes e conclua os serviços confirmados. + - - Marcações - - + + + Aguardam Aprovação + {pendingAppts.length} + + {pendingAppts.length > 0 ? ( + pendingAppts.map((appt) => renderAppointmentCard(appt, 'pending')) + ) : ( + Não há pedidos pendentes no momento. + )} + - {shopAppointments.length > 0 ? ( - shopAppointments.map((appt) => { - const svc = shop.services.find(s => s.id === appt.serviceId); - const barb = shop.barbers.find(b => b.id === appt.barberId); + + + Agendamentos Aprovados + {confirmedAppts.length} + + {confirmedAppts.length > 0 ? ( + confirmedAppts.map((appt) => renderAppointmentCard(appt, 'confirmed')) + ) : ( + Próximos serviços irão aparecer aqui. + )} + + + )} + + {activeTab === 'historico' && ( + + + Histórico de Agendamentos + {historyAppointments.length} registos + + {historyAppointments.length > 0 ? ( + historyAppointments.map((appt) => renderAppointmentCard(appt, 'history')) + ) : ( + + Nenhum registo no período selecionado. + + )} + + )} + + {activeTab === 'pedidos' && ( + + + Pedidos de Produtos + {shopOrders.length} no período + + {shopOrders.length > 0 ? ( + shopOrders.map((order) => { + const productItems = order.items.filter((i) => i.type === 'product'); + const productTotal = productItems.reduce((sum, item) => { + const prod = shop.products.find((p) => p.id === item.refId); + return sum + (prod?.price ?? 0) * item.qty; + }, 0); + const customer = users.find((u) => u.id === order.customerId); return ( - - - {appt.date.split(' ')[1]} - + + + {currency(productTotal)} + + {statusLabel[order.status] || order.status} + - - - {svc?.name || 'Serviço'} - {currency(appt.total)} - - Profissional: {barb?.name} - - {appt.status === 'pendente' && ( - updateAppointmentStatus(appt.id, 'confirmado')} style={styles.premiumActionBtn}> - Confirmar - - )} - {appt.status !== 'cancelado' && appt.status !== 'concluido' && ( - updateAppointmentStatus(appt.id, 'cancelado')} style={[styles.premiumActionBtn, { backgroundColor: colors.dangerSoft, borderColor: '#fecaca' }]}> - Cancelar - - )} - + + Cliente: {customer?.name || 'Cliente'} · {new Date(order.createdAt).toLocaleString('pt-PT')} + + {productItems.map((item) => { + const prod = shop.products.find((p) => p.id === item.refId); + return ( + + {prod?.name || 'Produto'} x{item.qty} + {currency((prod?.price ?? 0) * item.qty)} + + ); + })} + + {(['pendente', 'concluido'] as const).map((status) => ( + updateOrderStatus(order.id, status)} + style={[ + styles.premiumActionBtn, + order.status === status ? styles.acceptBtn : styles.rejectBtn, + ]} + > + + {status === 'pendente' ? 'Pendente' : 'Concluído'} + + + ))} ); }) ) : ( - Sem marcações para hoje. + Nenhum pedido de produtos no período. )} @@ -395,6 +782,109 @@ export default function Dashboard() { )} + {activeTab === 'estatisticas' && statsData && ( + + + Estatísticas + + Análise completa do desempenho da barbearia (todos os dados históricos) + + + + + {[ + { label: 'Total de Reservas', value: String(statsData.totalAppts) }, + { label: 'Concluídas', value: String(statsData.totalConcluidas) }, + { label: 'Canceladas', value: String(statsData.totalCanceladas) }, + { label: 'Taxa Conclusão', value: `${statsData.taxaConclusao}%` }, + ].map((kpi) => ( + + {kpi.label} + {kpi.value} + + ))} + + + + + Receita de Serviços + + {currency(statsData.revenueServicos)} + + Baseado nos agendamentos concluídos + + + Receita de Produtos + + {currency(statsData.revenueProdutos)} + + Baseado nos pedidos concluídos + + + + + Evolução de Reservas (últimas 8 semanas) + {statsData.weeklyData.map((week) => ( + + {week.name} + + {week.concluidas}/{week.reservas} concluídas + + + ))} + + + + Reservas por Dia da Semana + {statsData.byDayOfWeek.map((day) => { + const max = Math.max(...statsData.byDayOfWeek.map((d) => d.reservas), 1); + const pct = Math.round((day.reservas / max) * 100); + return ( + + {day.name} + + + + {day.reservas} + + ); + })} + + + {statsData.topServices.length > 0 && ( + + Serviços Mais Populares + {statsData.topServices.map((svc) => ( + + {svc.name} + {svc.value} reservas + + ))} + + )} + + {statsData.topBarbers.length > 0 && ( + + Ranking de Profissionais + {statsData.topBarbers.map((barber, index) => { + const maxQty = statsData.topBarbers[0]?.qty || 1; + const pct = Math.round((barber.qty / maxQty) * 100); + return ( + + #{index + 1} + {barber.name} + + + + {barber.qty} + + ); + })} + + )} + + )} + {activeTab === 'perfil' && ( Configurações do Espaço @@ -422,15 +912,11 @@ export default function Dashboard() { Horário de Funcionamento - {schedule.map((s, idx) => ( + {weeklySchedule.map((s) => ( {s.day} { - const next = [...schedule]; - next[idx] = { ...next[idx], closed: !next[idx].closed }; - setSchedule(next); - }} + onPress={() => updateWeeklySlot(s.day, { closed: !s.closed })} style={[styles.closedBtn, s.closed && styles.closedBtnActive]} > @@ -442,27 +928,99 @@ export default function Dashboard() { { - const next = [...schedule]; - next[idx] = { ...next[idx], open: t }; - setSchedule(next); - }} + onChangeText={(t) => updateWeeklySlot(s.day, { open: t })} /> - { - const next = [...schedule]; - next[idx] = { ...next[idx], close: t }; - setSchedule(next); - }} + onChangeText={(t) => updateWeeklySlot(s.day, { close: t })} /> )} ))} + + Exceções de Horário (Datas Específicas) + + Defina datas especiais em que a barbearia estará fechada (ex: Natal) ou aberta num dia normalmente fechado. + + + + + + {[ + [true, 'Fechado'], + [false, 'Aberto'], + ].map(([closed, label]) => ( + setExceptionClosed(Boolean(closed))} + > + + {label} + + + ))} + + + + + {exceptionSlots.length > 0 ? ( + exceptionSlots.map((slot) => ( + + + + {slot.date + ? new Date(`${slot.date}T00:00:00`).toLocaleDateString('pt-PT', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + : 'Data inválida'} + + + + {slot.closed ? 'Fechado' : 'Aberto'} + + + + removeException(slot.date)} style={styles.exceptionRemoveBtn}> + Remover + + + )) + ) : ( + Nenhuma exceção configurada. + )} + + @@ -531,6 +1089,93 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, borderBottomColor: colors.border, }, + periodRow: { + flexDirection: 'row', + gap: 8, + paddingHorizontal: 20, + paddingVertical: 10, + backgroundColor: colors.surface, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + periodChip: { + paddingHorizontal: 12, + paddingVertical: 7, + borderRadius: radius.md, + borderWidth: 1, + borderColor: colors.border, + backgroundColor: colors.surfaceMuted, + }, + periodChipActive: { + backgroundColor: colors.primarySoft, + borderColor: colors.primary, + }, + periodChipText: { + color: colors.textMuted, + fontSize: 11, + fontWeight: '700', + }, + periodChipTextActive: { + color: colors.primaryDark, + }, + sectionHeaderRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, + marginBottom: 8, + }, + linkAction: { + color: colors.primaryDark, + fontSize: 13, + fontWeight: '800', + }, + pendingSection: { + padding: 16, + gap: 12, + borderColor: colors.primarySoft, + backgroundColor: colors.primarySoft, + }, + acceptBtn: { + backgroundColor: colors.surfaceDark, + borderColor: colors.surfaceDark, + }, + rejectBtn: { + backgroundColor: colors.dangerSoft, + borderColor: '#fecaca', + }, + completeBtn: { + backgroundColor: colors.success, + borderColor: colors.success, + }, + orderCard: { + padding: 16, + gap: 10, + }, + orderTotal: { + color: colors.primaryDark, + fontSize: 18, + fontWeight: '900', + }, + orderItemRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: colors.backgroundAlt, + borderRadius: radius.sm, + paddingHorizontal: 10, + paddingVertical: 8, + }, + orderItemName: { + color: colors.textSoft, + fontSize: 13, + fontWeight: '600', + }, + orderItemPrice: { + color: colors.primaryDark, + fontSize: 13, + fontWeight: '800', + }, tabsScroll: { paddingHorizontal: 20, gap: 8, @@ -544,8 +1189,8 @@ const styles = StyleSheet.create({ backgroundColor: colors.surfaceMuted, }, tabItemActive: { - backgroundColor: colors.primary, - borderColor: colors.primary, + backgroundColor: colors.surfaceDark, + borderColor: colors.surfaceDark, }, tabText: { color: colors.textMuted, @@ -553,7 +1198,7 @@ const styles = StyleSheet.create({ fontWeight: '700', }, tabTextActive: { - color: colors.textInverse, + color: colors.brandAccent, }, content: { padding: 20, @@ -640,10 +1285,8 @@ const styles = StyleSheet.create({ borderColor: colors.border, }, agendaTicket: { - flexDirection: 'row', - padding: 0, - overflow: 'hidden', - height: 110, + padding: 14, + gap: 8, }, ticketSide: { width: 70, @@ -959,4 +1602,265 @@ const styles = StyleSheet.create({ marginTop: 16, fontWeight: '600', }, + sectionSubtitle: { + color: colors.textMuted, + fontSize: 13, + fontWeight: '500', + marginTop: 4, + lineHeight: 18, + }, + statsKpiGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + statsKpiCard: { + width: '48%', + padding: 14, + gap: 4, + }, + statsKpiLabel: { + color: colors.textMuted, + fontSize: 11, + fontWeight: '700', + }, + statsKpiValue: { + color: colors.text, + fontSize: 24, + fontWeight: '900', + }, + statsRevenueRow: { + gap: 12, + }, + statsRevenueCard: { + padding: 16, + gap: 6, + }, + statsRevenueTitle: { + color: colors.text, + fontSize: 15, + fontWeight: '800', + }, + statsRevenueValue: { + fontSize: 28, + fontWeight: '900', + }, + statsRevenueHint: { + color: colors.textMuted, + fontSize: 11, + fontWeight: '500', + }, + statsBlockCard: { + padding: 16, + gap: 10, + }, + statsBlockTitle: { + color: colors.text, + fontSize: 15, + fontWeight: '800', + marginBottom: 4, + }, + statsWeekRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 6, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + statsWeekLabel: { + color: colors.textSoft, + fontSize: 13, + fontWeight: '600', + }, + statsWeekValue: { + color: colors.text, + fontSize: 12, + fontWeight: '700', + }, + statsBarRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 8, + }, + statsBarLabel: { + width: 34, + color: colors.textMuted, + fontSize: 12, + fontWeight: '700', + }, + statsBarTrack: { + flex: 1, + height: 8, + backgroundColor: colors.backgroundAlt, + borderRadius: radius.pill, + overflow: 'hidden', + }, + statsBarFill: { + height: '100%', + backgroundColor: colors.primary, + borderRadius: radius.pill, + }, + statsBarValue: { + width: 24, + textAlign: 'right', + color: colors.text, + fontSize: 12, + fontWeight: '800', + }, + statsListRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + statsListName: { + flex: 1, + color: colors.text, + fontSize: 14, + fontWeight: '600', + }, + statsListValue: { + color: colors.textMuted, + fontSize: 12, + fontWeight: '800', + }, + statsRankRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 10, + }, + statsRankPos: { + width: 22, + color: colors.textSubtle, + fontSize: 12, + fontWeight: '800', + }, + statsRankName: { + width: 72, + color: colors.textSoft, + fontSize: 13, + fontWeight: '700', + }, + statsRankQty: { + width: 24, + textAlign: 'right', + color: colors.text, + fontSize: 12, + fontWeight: '800', + }, + exceptionSection: { + marginTop: 20, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: colors.border, + gap: 10, + }, + exceptionHint: { + color: colors.textMuted, + fontSize: 12, + lineHeight: 17, + fontWeight: '500', + }, + exceptionForm: { + gap: 10, + backgroundColor: colors.backgroundAlt, + borderRadius: radius.lg, + borderWidth: 1, + borderColor: colors.border, + padding: 12, + }, + exceptionDateInput: { + marginBottom: 0, + }, + exceptionToggleRow: { + flexDirection: 'row', + gap: 8, + }, + exceptionToggleBtn: { + flex: 1, + paddingVertical: 10, + borderRadius: radius.md, + borderWidth: 1, + borderColor: colors.border, + backgroundColor: colors.surface, + alignItems: 'center', + }, + exceptionToggleBtnActive: { + backgroundColor: colors.surfaceDark, + borderColor: colors.surfaceDark, + }, + exceptionToggleText: { + color: colors.textMuted, + fontSize: 13, + fontWeight: '700', + }, + exceptionToggleTextActive: { + color: colors.brandAccent, + }, + exceptionItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 10, + padding: 12, + borderRadius: radius.md, + borderWidth: 1, + borderColor: colors.border, + backgroundColor: colors.surface, + }, + exceptionItemInfo: { + flex: 1, + gap: 6, + }, + exceptionItemDate: { + color: colors.textSoft, + fontSize: 14, + fontWeight: '700', + }, + exceptionBadge: { + alignSelf: 'flex-start', + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: radius.pill, + }, + exceptionBadgeClosed: { + backgroundColor: colors.dangerSoft, + }, + exceptionBadgeOpen: { + backgroundColor: colors.successSoft, + }, + exceptionBadgeText: { + fontSize: 10, + fontWeight: '800', + textTransform: 'uppercase', + }, + exceptionBadgeTextClosed: { + color: colors.danger, + }, + exceptionBadgeTextOpen: { + color: colors.success, + }, + exceptionRemoveBtn: { + paddingHorizontal: 10, + paddingVertical: 8, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: '#fecaca', + backgroundColor: colors.dangerSoft, + }, + exceptionRemoveText: { + color: colors.danger, + fontSize: 12, + fontWeight: '800', + }, + exceptionEmpty: { + color: colors.textMuted, + fontSize: 12, + fontStyle: 'italic', + }, }); diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx index 34d5cc2..0f2e041 100644 --- a/src/pages/Explore.tsx +++ b/src/pages/Explore.tsx @@ -1,9 +1,10 @@ import React, { useMemo, useState } from 'react'; -import { Image, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useApp } from '../context/AppContext'; +import { ShopCard } from '../components/ShopCard'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { RootStackParamList } from '../navigation/types'; @@ -12,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons'; export default function Explore() { const navigation = useNavigation>(); - const { shops, shopsReady, user, toggleFavorite, isFavorite } = useApp(); + const { shops, shopsReady, toggleFavorite, isFavorite } = useApp(); const [query, setQuery] = useState(''); const [filter, setFilter] = useState<'todas' | 'top' | 'servicos'>('todas'); @@ -34,49 +35,32 @@ export default function Explore() { }); }, [shops, query, filter]); - const featuredShops = filtered.slice(0, 3); - - const goToProfile = () => { - if (!user) { - navigation.navigate('Login'); - return; - } - navigation.getParent()?.navigate('ProfileTab' as never); - }; - return ( - - {/* Modern Header Section */} - - - - {user ? `Olá, ${user.name.split(' ')[0]} 👋` : 'Bem-vindo 👋'} - - Encontra um espaço + + + + As Nossas Barbearias - - - - {user ? user.name.charAt(0).toUpperCase() : 'S'} - - - + + Ver Barbearias + + + Descubra barbearias exclusivas e reserve o seu próximo corte em segundos. + + + {filtered.length} Espaços Disponíveis + - {/* Clean Integrated Search Box */} - - - + + + @@ -87,7 +71,6 @@ export default function Explore() { )} - {/* Quick Filters Scroll */} {[ ['todas', 'Melhor avaliação'], @@ -104,7 +87,7 @@ export default function Explore() { ))} - + {!shopsReady ? ( @@ -121,155 +104,17 @@ export default function Explore() { ) : ( - <> - {/* Featured Section */} - {featuredShops.length > 0 && ( - - - Em destaque - {filtered.length} espaços - - - {featuredShops.map((shop) => { - const isFav = isFavorite(shop.id); - return ( - navigation.navigate('ShopDetails', { shopId: shop.id })} - > - - {shop.imageUrl ? ( - - ) : ( - - - - )} - - {/* Rating Badge Overlay */} - - - {(shop.rating || 0).toFixed(1)} - - - {/* Floating Favorite Heart Button */} - { - e.stopPropagation(); - toggleFavorite(shop.id); - }} - > - - - - - {shop.name} - - - - {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'} - - - - {(shop.services || []).length} serv. • {(shop.barbers || []).length} barb. - - - - - - - ); - })} - - - )} - - {/* All Spaces Section */} - - - Todos os espaços - {filtered.length} - - - {filtered.map((shop) => { - const isFav = isFavorite(shop.id); - return ( - navigation.navigate('ShopDetails', { shopId: shop.id })} - > - - {shop.imageUrl ? ( - - ) : ( - - - - )} - {/* Floating Favorite Button on list image */} - { - e.stopPropagation(); - toggleFavorite(shop.id); - }} - > - - - - - - - {shop.name} - - - {(shop.rating || 0).toFixed(1)} - - - - - - - {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'} - - - - - - - {(shop.services || []).length} serv. - - - - {(shop.barbers || []).length} barb. - - - - - - - - - ); - })} - - - + + {filtered.map((shop) => ( + toggleFavorite(shop.id)} + onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })} + /> + ))} + )} @@ -283,461 +128,105 @@ const styles = StyleSheet.create({ }, content: { padding: 20, - gap: 22, + gap: 20, paddingBottom: 40, }, - header: { + hero: { + gap: 8, + }, + kicker: { + alignSelf: 'flex-start', flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', - marginTop: 8, + gap: 6, + backgroundColor: colors.primarySoft, + paddingHorizontal: 12, + paddingVertical: 5, + borderRadius: radius.pill, }, - headerCopy: { - flex: 1, + kickerText: { + color: colors.primaryDark, + fontSize: 10, + fontWeight: '900', + textTransform: 'uppercase', + letterSpacing: 1.2, }, - greeting: { + title: { + color: colors.text, + fontSize: 32, + fontWeight: '900', + letterSpacing: -0.8, + textTransform: 'uppercase', + }, + titleAccent: { + color: colors.primary, + }, + subtitle: { color: colors.textMuted, fontSize: 14, - fontWeight: '600', - marginBottom: 4, - letterSpacing: 0.3, + fontWeight: '500', + lineHeight: 20, + maxWidth: 340, }, - headline: { - color: colors.text, - fontSize: 28, - fontWeight: '900', - letterSpacing: -0.5, - }, - avatarButton: { - shadowColor: colors.primary, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 4, - }, - avatarGradient: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: colors.primary, - alignItems: 'center', - justifyContent: 'center', - borderWidth: 2, - borderColor: colors.surface, - }, - avatarText: { - color: colors.textInverse, - fontSize: 18, + countLabel: { + marginTop: 4, + color: colors.textSubtle, + fontSize: 12, fontWeight: '800', + textTransform: 'uppercase', + letterSpacing: 0.8, }, - searchSection: { - gap: 12, + countValue: { + color: colors.text, }, - searchBox: { + searchCard: { + padding: 8, + borderRadius: radius.xl, + borderWidth: 0, + ...shadows.card, + }, + searchRow: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.surface, - borderRadius: radius.md, - borderWidth: 1, - borderColor: 'rgba(15,118,110,0.1)', - paddingHorizontal: 16, gap: 10, - height: 54, - shadowColor: '#102018', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.04, - shadowRadius: 8, - elevation: 2, + paddingHorizontal: 12, + paddingVertical: 4, }, searchInput: { flex: 1, - height: '100%', + height: 48, color: colors.text, - fontSize: 15, + fontSize: 16, fontWeight: '500', }, filtersScroll: { gap: 8, - paddingVertical: 2, + paddingHorizontal: 8, + paddingBottom: 8, + paddingTop: 4, }, filterChip: { borderRadius: radius.pill, - backgroundColor: colors.surface, - paddingHorizontal: 16, - paddingVertical: 10, + backgroundColor: colors.backgroundAlt, + paddingHorizontal: 14, + paddingVertical: 9, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.08)', - shadowColor: '#102018', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.02, - shadowRadius: 4, - elevation: 1, + borderColor: colors.border, }, filterChipActive: { - backgroundColor: colors.primary, - borderColor: colors.primary, + backgroundColor: colors.surfaceDark, + borderColor: colors.surfaceDark, }, filterChipText: { color: colors.textMuted, - fontSize: 13, + fontSize: 12, fontWeight: '700', }, filterChipTextActive: { - color: colors.textInverse, + color: colors.brandAccent, }, - categoriesSection: { - gap: 12, - marginTop: 4, - }, - categoriesScroll: { - gap: 14, - paddingRight: 10, - paddingVertical: 4, - }, - categoryItem: { - alignItems: 'center', - gap: 8, - width: 72, - }, - categoryIconBg: { - width: 58, - height: 58, - borderRadius: 29, - backgroundColor: colors.surface, - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderColor: 'rgba(15,118,110,0.08)', - shadowColor: '#102018', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.05, - shadowRadius: 8, - elevation: 2, - }, - categoryIconBgActive: { - backgroundColor: colors.primary, - borderColor: colors.primary, - }, - categoryLabel: { - fontSize: 12, - fontWeight: '600', - color: colors.textMuted, - textAlign: 'center', - }, - categoryLabelActive: { - color: colors.primary, - fontWeight: '800', - }, - promoBanner: { - flexDirection: 'row', - backgroundColor: colors.surfaceDark, - borderRadius: radius.lg, - overflow: 'hidden', - shadowColor: colors.primaryDark, - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.12, - shadowRadius: 12, - elevation: 5, - marginTop: 6, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.04)', - }, - promoContent: { - flex: 1.2, - padding: 20, - gap: 8, - }, - promoBadge: { - alignSelf: 'flex-start', - backgroundColor: 'rgba(255,255,255,0.12)', - paddingHorizontal: 8, - paddingVertical: 3, - borderRadius: radius.xs, - }, - promoBadgeText: { - color: '#e2f4f1', - fontSize: 10, - fontWeight: '800', - letterSpacing: 1, - }, - promoTitle: { - color: colors.textInverse, - fontSize: 18, - fontWeight: '900', - lineHeight: 22, - }, - promoSubtitle: { - color: 'rgba(248,251,248,0.72)', - fontSize: 12, - lineHeight: 16, - fontWeight: '500', - }, - promoButton: { - alignSelf: 'flex-start', - backgroundColor: colors.surface, - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: radius.sm, - marginTop: 6, - }, - promoButtonText: { - color: colors.surfaceDark, - fontSize: 12, - fontWeight: '800', - }, - promoVisual: { - flex: 0.8, - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - }, - promoBackgroundIcon: { - position: 'absolute', - right: -10, - bottom: -10, - }, - sectionHeaderRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 12, - }, - sectionTitle: { - color: colors.text, - fontSize: 18, - fontWeight: '900', - letterSpacing: -0.3, - }, - countBadge: { - color: colors.primary, - backgroundColor: colors.primarySoft, - fontSize: 11, - fontWeight: '800', - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: radius.pill, - textTransform: 'uppercase', - }, - featuredSection: { - marginTop: 4, - }, - featuredList: { + grid: { gap: 16, - paddingRight: 10, - paddingVertical: 4, - }, - featuredCard: { - width: 250, - backgroundColor: colors.surface, - borderRadius: radius.lg, - borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', - overflow: 'hidden', - shadowColor: '#102018', - shadowOffset: { width: 0, height: 6 }, - shadowOpacity: 0.05, - shadowRadius: 10, - elevation: 3, - }, - featuredImageWrapper: { - position: 'relative', - width: '100%', - height: 130, - }, - featuredImage: { - width: '100%', - height: '100%', - }, - featuredFallback: { - width: '100%', - height: '100%', - backgroundColor: colors.primarySoft, - alignItems: 'center', - justifyContent: 'center', - }, - ratingBadgeOverlay: { - position: 'absolute', - bottom: 10, - left: 10, - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(12,20,17,0.7)', - borderRadius: radius.pill, - paddingHorizontal: 8, - paddingVertical: 3.5, - gap: 4, - borderWidth: 0.5, - borderColor: 'rgba(255,255,255,0.15)', - }, - ratingBadgeText: { - color: colors.textInverse, - fontSize: 11, - fontWeight: '800', - }, - favoriteBtnFloating: { - position: 'absolute', - top: 10, - right: 10, - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: 'rgba(12,20,17,0.5)', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 0.5, - borderColor: 'rgba(255,255,255,0.15)', - }, - featuredCardBody: { - padding: 14, - gap: 6, - }, - featuredShopName: { - fontSize: 16, - fontWeight: '800', - color: colors.text, - }, - infoRowInline: { - flexDirection: 'row', - alignItems: 'center', - gap: 5, - }, - addressText: { - flex: 1, - color: colors.textMuted, - fontWeight: '500', - fontSize: 13, - }, - cardFooterRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginTop: 4, - borderTopWidth: 1, - borderTopColor: 'rgba(15,118,110,0.05)', - paddingTop: 8, - }, - featuredStatsText: { - color: colors.textSubtle, - fontSize: 12, - fontWeight: '600', - }, - goBtn: { - width: 22, - height: 22, - borderRadius: 11, - backgroundColor: colors.primary, - alignItems: 'center', - justifyContent: 'center', - }, - allSpacesSection: { - marginTop: 4, - }, - allSpacesList: { - gap: 12, - }, - shopCard: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: radius.lg, - backgroundColor: colors.surface, - borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', - padding: 10, - gap: 12, - shadowColor: '#102018', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.03, - shadowRadius: 8, - elevation: 2, - }, - shopImageWrapper: { - position: 'relative', - width: 82, - height: 82, - borderRadius: radius.md, - overflow: 'hidden', - }, - shopImage: { - width: '100%', - height: '100%', - }, - shopImageFallback: { - width: '100%', - height: '100%', - backgroundColor: colors.primarySoft, - alignItems: 'center', - justifyContent: 'center', - }, - favoriteBtnListFloating: { - position: 'absolute', - top: 4, - right: 4, - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: 'rgba(12,20,17,0.5)', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 0.5, - borderColor: 'rgba(255,255,255,0.15)', - }, - shopBody: { - flex: 1, - gap: 4, - }, - shopNameRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - gap: 8, - }, - shopName: { - flex: 1, - fontSize: 16, - fontWeight: '800', - color: colors.text, - }, - ratingBadgeSmall: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.accentSoft, - borderRadius: radius.pill, - paddingHorizontal: 7, - paddingVertical: 2, - gap: 3, - }, - ratingTextSmall: { - color: '#7a4310', - fontSize: 10, - fontWeight: '800', - }, - shopStatsRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - marginTop: 2, - }, - statTag: { - flexDirection: 'row', - alignItems: 'center', - gap: 3, - backgroundColor: 'rgba(15,118,110,0.05)', - borderRadius: radius.xs, - paddingHorizontal: 6, - paddingVertical: 2, - }, - statTagText: { - color: colors.primary, - fontSize: 10, - fontWeight: '700', - }, - chevronIconContainer: { - width: 26, - height: 26, - borderRadius: 13, - backgroundColor: 'rgba(15,118,110,0.05)', - alignItems: 'center', - justifyContent: 'center', }, emptyState: { alignItems: 'center', diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx index 3831485..4d35d56 100644 --- a/src/pages/Landing.tsx +++ b/src/pages/Landing.tsx @@ -294,7 +294,7 @@ const styles = StyleSheet.create({ fontWeight: '900', }, rating: { - color: '#7a4310', + color: colors.primaryDark, backgroundColor: colors.accentSoft, borderRadius: radius.pill, paddingHorizontal: 10, diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index f810e52..1fb0f25 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -250,6 +250,9 @@ export default function Profile() { {shop?.name || 'Barbearia'} + + {shop?.services.find((s) => s.id === appt.serviceId)?.name || 'Serviço'} · {shop?.services.find((s) => s.id === appt.serviceId)?.duration || '—'} min + {dateParts[1]} • {currency(appt.total)} @@ -558,7 +561,7 @@ const styles = StyleSheet.create({ height: 72, borderRadius: 36, borderWidth: 2.5, - borderColor: 'rgba(15,118,110,0.35)', + borderColor: 'rgba(245,158,11,0.35)', alignItems: 'center', justifyContent: 'center', padding: 2, @@ -620,7 +623,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, profileStat: { @@ -688,12 +691,12 @@ const styles = StyleSheet.create({ padding: 14, marginRight: 12, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, gap: 6, ...shadows.soft, }, notifUnread: { - borderColor: 'rgba(15,118,110,0.2)', + borderColor: 'rgba(245,158,11,0.2)', backgroundColor: colors.primarySoft, }, notifHeader: { @@ -739,7 +742,7 @@ const styles = StyleSheet.create({ borderRadius: radius.pill, backgroundColor: colors.surface, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, }, tabActive: { backgroundColor: colors.primary, @@ -791,7 +794,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, agendaTop: { @@ -805,7 +808,7 @@ const styles = StyleSheet.create({ alignItems: 'center', minWidth: 52, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, overflow: 'hidden', }, calendarHeader: { @@ -873,7 +876,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, shopIcon: { @@ -884,7 +887,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderWidth: 0.5, - borderColor: 'rgba(15,118,110,0.1)', + borderColor: colors.borderStrong, }, shopIconText: { color: colors.primary, @@ -921,7 +924,7 @@ const styles = StyleSheet.create({ gap: 3, }, shopRatingText: { - color: '#7a4310', + color: colors.primaryDark, fontWeight: '800', fontSize: 11, }, @@ -933,7 +936,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, orderTop: { @@ -970,7 +973,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, reviewHeader: { @@ -1007,7 +1010,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, statCardFull: { @@ -1017,7 +1020,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, ...shadows.soft, }, statIconContainer: { @@ -1071,7 +1074,7 @@ const styles = StyleSheet.create({ height: 90, textAlignVertical: 'top', borderWidth: 1, - borderColor: 'rgba(15,118,110,0.08)', + borderColor: colors.borderStrong, }, reviewActions: { flexDirection: 'row', @@ -1083,7 +1086,7 @@ const styles = StyleSheet.create({ marginTop: 20, paddingTop: 20, borderTopWidth: 1, - borderTopColor: 'rgba(15,118,110,0.08)', + borderTopColor: colors.borderStrong, alignItems: 'center', }, /* ── Premium Client Layout Styles ── */ @@ -1105,7 +1108,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, gap: 16, ...shadows.soft, }, @@ -1117,7 +1120,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderWidth: 1, - borderColor: 'rgba(15,118,110,0.1)', + borderColor: colors.borderStrong, }, dateMonthPremium: { color: colors.primaryDark, @@ -1136,6 +1139,12 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '800', }, + agendaServicePremium: { + color: colors.textMuted, + fontSize: 12, + fontWeight: '600', + marginTop: 2, + }, agendaTimePremium: { color: colors.textMuted, fontSize: 13, @@ -1159,7 +1168,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.04)', + borderColor: colors.backgroundAlt, gap: 12, }, emptyIconCircle: { @@ -1183,7 +1192,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, gap: 12, }, shopImageMini: { @@ -1218,7 +1227,7 @@ const styles = StyleSheet.create({ gap: 4, }, shopRatingTextPremium: { - color: '#7a4310', + color: colors.primaryDark, fontSize: 12, fontWeight: '900', }, @@ -1227,7 +1236,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, }, orderTopPremium: { flexDirection: 'row', diff --git a/src/pages/ShopDetails.tsx b/src/pages/ShopDetails.tsx index daf5578..28e6fd4 100644 --- a/src/pages/ShopDetails.tsx +++ b/src/pages/ShopDetails.tsx @@ -9,11 +9,12 @@ import { Button } from '../components/ui/Button'; import { currency } from '../lib/format'; import { RootStackParamList } from '../navigation/types'; import { colors, radius, shadows } from '../lib/theme'; +import { ShopSchedule } from '../types'; import { Ionicons } from '@expo/vector-icons'; type Tab = 'servicos' | 'barbeiros' | 'produtos' | 'detalhes'; -const defaultSchedule = [ +const defaultSchedule: ShopSchedule[] = [ { day: 'Segunda-feira', open: '09:00', close: '19:30' }, { day: 'Terça-feira', open: '09:00', close: '19:30' }, { day: 'Quarta-feira', open: '09:00', close: '19:30' }, @@ -312,6 +313,52 @@ export default function ShopDetails() { ) : ( Contacto telefónico não disponível. )} + + {shop.contacts?.phone2 ? ( + Linking.openURL(`tel:${shop.contacts?.phone2}`)} style={styles.phoneLinkBox} activeOpacity={0.7}> + + {shop.contacts.phone2} + + ) : null} + + {shop.paymentMethods && shop.paymentMethods.length > 0 && ( + <> + + + + Formas de Pagamento + + {shop.paymentMethods.join(', ')} + + )} + + {(shop.socialNetworks?.whatsapp || shop.socialNetworks?.instagram || shop.socialNetworks?.facebook) && ( + <> + + + + Redes Sociais + + {shop.socialNetworks?.whatsapp ? ( + Linking.openURL(`https://wa.me/${shop.socialNetworks?.whatsapp}`)} style={styles.phoneLinkBox}> + + WhatsApp + + ) : null} + {shop.socialNetworks?.instagram ? ( + Linking.openURL(`https://instagram.com/${shop.socialNetworks?.instagram?.replace('@', '')}`)} style={styles.phoneLinkBox}> + + Instagram + + ) : null} + {shop.socialNetworks?.facebook ? ( + Linking.openURL(shop.socialNetworks!.facebook!)} style={styles.phoneLinkBox}> + + Facebook + + ) : null} + + )} )} @@ -388,8 +435,8 @@ const styles = StyleSheet.create({ padding: 20, gap: 14, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', - shadowColor: '#102018', + borderColor: colors.border, + shadowColor: colors.surfaceDark, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, shadowRadius: 16, @@ -410,7 +457,7 @@ const styles = StyleSheet.create({ gap: 4, }, ratingValue: { - color: '#7a4310', + color: colors.primaryDark, fontSize: 12, fontWeight: '900', }, @@ -457,7 +504,7 @@ const styles = StyleSheet.create({ borderRadius: radius.md, padding: 10, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.05)', + borderColor: colors.border, }, quickValue: { color: colors.text, @@ -498,7 +545,7 @@ const styles = StyleSheet.create({ borderRadius: radius.pill, backgroundColor: colors.surface, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.05)', + borderColor: colors.border, }, tabItemActive: { backgroundColor: colors.primary, @@ -525,9 +572,9 @@ const styles = StyleSheet.create({ gap: 12, borderRadius: radius.md, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, backgroundColor: colors.surface, - shadowColor: '#102018', + shadowColor: colors.surfaceDark, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.02, shadowRadius: 6, @@ -576,7 +623,7 @@ const styles = StyleSheet.create({ gap: 14, borderRadius: radius.md, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, backgroundColor: colors.surface, }, barberAvatar: { @@ -587,7 +634,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderWidth: 1, - borderColor: 'rgba(15,118,110,0.1)', + borderColor: colors.borderStrong, }, avatarTxt: { color: colors.primary, @@ -621,7 +668,7 @@ const styles = StyleSheet.create({ gap: 10, borderRadius: radius.md, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, backgroundColor: colors.surface, }, prodHeader: { @@ -672,7 +719,7 @@ const styles = StyleSheet.create({ gap: 14, borderRadius: radius.lg, borderWidth: 1, - borderColor: 'rgba(15,118,110,0.06)', + borderColor: colors.border, backgroundColor: colors.surface, }, detailTitleRow: { @@ -686,6 +733,11 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '800', }, + detailBody: { + color: colors.textMuted, + fontSize: 14, + lineHeight: 20, + }, scheduleList: { gap: 8, }, @@ -716,7 +768,7 @@ const styles = StyleSheet.create({ }, divider: { height: 1, - backgroundColor: 'rgba(15,118,110,0.08)', + backgroundColor: colors.borderStrong, marginVertical: 6, }, phoneLinkBox: { @@ -729,7 +781,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 14, borderRadius: radius.sm, borderWidth: 0.5, - borderColor: 'rgba(15,118,110,0.1)', + borderColor: colors.borderStrong, }, contactLink: { color: colors.primaryDark,