diff --git a/App.tsx b/App.tsx index 9b2795b..a790aa7 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,7 @@ export default function App() { - + ); diff --git a/src/pages/Booking.tsx b/src/pages/Booking.tsx index 13f5576..926e285 100644 --- a/src/pages/Booking.tsx +++ b/src/pages/Booking.tsx @@ -1,321 +1,183 @@ -/** - * @file Booking.tsx - * @description Página de agendamento reformulada para um fluxo multi-etapas (Wizard). - * Melhora a UX separando seleções e introduzindo seletor de data aprimorado. - */ -import React, { useState, useMemo, useEffect } from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, FlatList } from 'react-native'; +import React, { useState, useMemo } from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRoute, useNavigation } from '@react-navigation/native'; import { useApp } from '../context/AppContext'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; -import { Stepper } from '../components/ui/Stepper'; import { currency } from '../lib/format'; 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, waitlists, joinWaitlist } = useApp(); + const { shops, createAppointment, user, appointments, joinWaitlist } = useApp(); const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]); - // Gestão de Steps const [step, setStep] = useState(initialServiceId ? 2 : 1); - const steps = [ - { id: 1, label: 'Serviço' }, - { id: 2, label: 'Barbeiro' }, - { id: 3, label: 'Horário' }, - { id: 4, label: 'Confirmação' }, - ]; - - // Estados de Seleção const [serviceId, setService] = useState(initialServiceId || ''); const [barberId, setBarber] = useState(''); const [date, setDate] = useState(new Date().toISOString().split('T')[0]); const [slot, setSlot] = useState(''); - const [reminderMinutes, setReminderMinutes] = useState(60); // 1h por padrão - - const reminderOptions = [ - { label: '10 min', value: 10 }, - { label: '30 min', value: 30 }, - { label: '1 hora', value: 60 }, - { label: '24 horas', value: 1440 }, - ]; - - // Geração de datas (próximos 14 dias) - const availableDates = useMemo(() => { - const dates = []; - const today = new Date(); - for (let i = 0; i < 14; i++) { - const d = new Date(); - d.setDate(today.getDate() + i); - dates.push({ - full: d.toISOString().split('T')[0], - day: d.getDate(), - weekday: d.toLocaleDateString('pt-PT', { weekday: 'short' }).replace('.', ''), - }); - } - return dates; - }, []); + const [reminderMinutes, setReminderMinutes] = useState(60); const selectedService = shop?.services.find((s) => s.id === serviceId); const selectedBarber = shop?.barbers.find((b) => b.id === barberId); - const generateDefaultSlots = (): string[] => { - const slots: string[] = []; - for (let hour = 9; hour <= 18; hour++) { - slots.push(`${hour.toString().padStart(2, '0')}:00`); + const availableDates = useMemo(() => { + const dates = []; + const today = new Date(); + for (let i = 0; i < 14; i++) { + const d = new Date(); + d.setDate(today.getDate() + i); + dates.push({ + full: d.toISOString().split('T')[0], + day: d.getDate(), + weekday: d.toLocaleDateString('pt-PT', { weekday: 'short' }).replace('.', '').toUpperCase(), + }); } - return slots; - }; + return dates; + }, []); const processedSlots = useMemo(() => { if (!selectedBarber || !date) return []; - const specificSchedule = selectedBarber.schedule.find((s) => s.day === date); - let 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); - - return slots.map(time => { - const isBooked = bookedSlots.includes(time); - const isSelected = slot === time; - return { time, isBooked, isSelected }; - }); - }, [selectedBarber, date, barberId, appointments, slot]); - - if (!shop) { - return ( - - Barbearia não encontrada - - ); - } - - const canNext = () => { - if (step === 1) return !!serviceId; - if (step === 2) return !!barberId; - if (step === 3) return !!date && !!slot; - return true; - }; + 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]); const submit = async () => { - if (!user) { - Alert.alert('Login necessário', 'Faça login para agendar'); - navigation.navigate('Login' as never); - return; - } - + if (!user) { navigation.navigate('Login' as never); return; } const appt = await createAppointment({ - shopId: shop.id, + shopId: shop!.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}`, reminderMinutes }); - if (appt) { - Alert.alert('Sucesso', 'O seu agendamento foi confirmado. Receberá um alerta no telemóvel conforme configurado.'); + Alert.alert('Sucesso', 'O seu agendamento foi confirmado.'); navigation.navigate('Profile' as never); - } else { - Alert.alert('Erro', 'Ocorreu um problema ao criar o agendamento.'); } }; - const renderStepContent = () => { - switch (step) { - case 1: - return ( - - O que vamos fazer hoje? - - {shop.services.map((s) => ( - setService(s.id)} - > - - {s.name} - {s.duration} min - - {currency(s.price)} - - ))} - - - ); - case 2: - return ( - - Com quem prefere? - - {shop.barbers.map((b) => ( - setBarber(b.id)} - > - - {b.name.charAt(0).toUpperCase()} - - - {b.name} - {b.specialties.join(', ')} - - {barberId === b.id && } - - ))} - - - ); - case 3: - return ( - - Quando? - - {/* Seletor de Data Horizontal */} - Escolha o dia - - {availableDates.map((d) => ( - { setDate(d.full); setSlot(''); }} - > - {d.weekday} - {d.day} - - ))} - - - Horários Disponíveis - - {processedSlots.length > 0 ? ( - processedSlots.map((s) => ( - setSlot(s.time)} - > - - {s.time} - - - )) - ) : ( - Sem horários para este barbeiro neste dia. - )} - - - {processedSlots.length > 0 && !processedSlots.some(s => !s.isBooked) && ( - - Esgotado! Queres ser avisado se alguém cancelar? - - - )} - - ); - case 4: - return ( - - Tudo certo? - - - - Serviço - {selectedService?.name} - - - Profissional - {selectedBarber?.name} - - - Data e Hora - {date} às {slot} - - - - Total - {currency(selectedService?.price || 0)} - - - - - Configurar Alerta Push - Quando queres receber o lembrete no telemóvel? - - {reminderOptions.map(opt => ( - setReminderMinutes(opt.value)} - > - - {opt.label} - - - ))} - - - - ); - default: - return null; - } - }; + if (!shop) return null; return ( - - - - {renderStepContent()} - - - - {step > 1 && ( - - )} - + {/* Header Estilizado */} + + step > 1 ? setStep(s => s - 1) : navigation.goBack()} style={styles.backBtn}> + + + + Agendamento + Etapa {step} de 4 + + + + + {step === 1 && ( + + Escolha o serviço + {shop.services.map(s => ( + { setService(s.id); setStep(2); }}> + + + {s.name} + {s.duration} min + + {currency(s.price)} + + + ))} + + )} + + {step === 2 && ( + + Com quem prefere? + {shop.barbers.map(b => ( + { setBarber(b.id); setStep(3); }}> + + {b.name.charAt(0)} + + {b.name} + {b.specialties[0] || 'Profissional'} + + + + ))} + + )} + + {step === 3 && ( + + Escolha o dia + + {availableDates.map(d => ( + setDate(d.full)} style={[styles.dateItem, date === d.full && styles.dateItemActive]}> + {d.weekday} + {d.day} + + ))} + + + Horários disponíveis + + {processedSlots.map(s => ( + setSlot(s.time)} + style={[styles.slotItem, slot === s.time && styles.slotActive, s.isBooked && styles.slotBooked]} + > + + {s.time} + + + ))} + + + {slot !== '' && ( + + )} + + )} + + {step === 4 && ( + + Confirmar detalhes + + Serviço{selectedService?.name} + Barbeiro{selectedBarber?.name} + Data{date} + Hora{slot} + + Total{currency(selectedService?.price || 0)} + + + Lembrete + + {[15, 30, 60, 120].map(m => ( + setReminderMinutes(m)} style={[styles.notifBtn, reminderMinutes === m && styles.notifBtnActive]}> + + {m >= 60 ? `${m/60}h antes` : `${m}m antes`} + + + ))} + + + + + )} + ); } @@ -323,167 +185,124 @@ export default function Booking() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8fafc', + backgroundColor: '#0a0a0f', }, - mainScroll: { - flex: 1, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 10, + }, + backBtn: { + width: 40, + height: 40, + borderRadius: 12, + backgroundColor: '#141420', + alignItems: 'center', + justifyContent: 'center', + }, + backIcon: { + color: '#f8fafc', + fontSize: 20, + fontWeight: '700', + }, + headerTitleBox: { + alignItems: 'center', + }, + headerTitle: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '800', + }, + headerSubtitle: { + color: '#64748b', + fontSize: 12, + fontWeight: '600', }, scrollContent: { - padding: 16, + padding: 20, paddingBottom: 40, }, - stepContent: { - width: '100%', + stepBox: { + gap: 12, }, - stepTitle: { - fontSize: 22, - fontWeight: 'bold', - color: '#0f172a', - marginBottom: 20, - textAlign: 'center', - }, - subTitle: { - fontSize: 14, - fontWeight: '700', - color: '#64748b', + stepLabel: { + color: '#94a3b8', + fontSize: 12, + fontWeight: '800', textTransform: 'uppercase', letterSpacing: 1, - marginTop: 10, - marginBottom: 12, + marginBottom: 8, }, - // Step 1 - Serviços - serviceGrid: { - gap: 12, - }, - serviceCard: { - backgroundColor: '#fff', - borderRadius: 16, - padding: 20, + choiceCard: { flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', - borderWidth: 2, - borderColor: 'transparent', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 4, - elevation: 2, - }, - serviceCardActive: { - borderColor: '#6366f1', - backgroundColor: '#f5f3ff', - }, - serviceInfo: { - flex: 1, - }, - serviceName: { - fontSize: 16, - fontWeight: '700', - color: '#1e293b', - }, - serviceNameActive: { - color: '#6366f1', - }, - serviceDuration: { - fontSize: 12, - color: '#94a3b8', - marginTop: 4, - }, - servicePrice: { - fontSize: 16, - fontWeight: '800', - color: '#0f172a', - }, - servicePriceActive: { - color: '#6366f1', - }, - // Step 2 - Barbeiros - barberList: { - gap: 12, - }, - barberItem: { - backgroundColor: '#fff', - borderRadius: 16, padding: 16, - flexDirection: 'row', - alignItems: 'center', - borderWidth: 2, + gap: 14, + borderWidth: 1.5, borderColor: 'transparent', }, - barberItemActive: { + choiceCardActive: { borderColor: '#6366f1', - backgroundColor: '#f5f3ff', + backgroundColor: 'rgba(99,102,241,0.05)', }, - avatarPlaceholder: { - width: 50, - height: 50, - borderRadius: 25, - backgroundColor: '#f1f5f9', - alignItems: 'center', - justifyContent: 'center', - marginRight: 16, - }, - avatarActive: { - backgroundColor: '#6366f1', - }, - avatarText: { - fontSize: 20, - fontWeight: 'bold', - color: '#94a3b8', - }, - barberDetails: { - flex: 1, - }, - barberName: { + choiceName: { + color: '#f8fafc', fontSize: 16, fontWeight: '700', - color: '#1e293b', }, - barberNameActive: { - color: '#6366f1', + choiceMeta: { + color: '#64748b', + fontSize: 13, }, - barberSpecialty: { - fontSize: 12, - color: '#94a3b8', + choicePrice: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', }, - // Step 3 - Data e Hora - dateScroll: { - marginBottom: 20, - paddingBottom: 4, - }, - dateButton: { - width: 60, - height: 80, - backgroundColor: '#fff', - borderRadius: 16, - marginRight: 8, + avatarMini: { + width: 44, + height: 44, + borderRadius: 14, + backgroundColor: '#1c1c2e', alignItems: 'center', justifyContent: 'center', - borderWidth: 2, - borderColor: 'transparent', }, - dateButtonActive: { - borderColor: '#6366f1', + avatarTxt: { + color: '#6366f1', + fontSize: 18, + fontWeight: '900', + }, + dateList: { + gap: 10, + }, + dateItem: { + width: 64, + height: 80, + borderRadius: 16, + backgroundColor: '#141420', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', + }, + dateItemActive: { backgroundColor: '#6366f1', + borderColor: '#6366f1', }, - dayName: { + dateWeek: { + color: '#475569', fontSize: 10, - textTransform: 'uppercase', - color: '#94a3b8', - fontWeight: 'bold', - }, - dayNameActive: { - color: '#fff', - opacity: 0.8, - }, - dayNum: { - fontSize: 20, fontWeight: '800', - color: '#1e293b', + }, + dateDay: { + color: '#f8fafc', + fontSize: 20, + fontWeight: '900', marginTop: 4, }, - dayNumActive: { + dateTextActive: { color: '#fff', }, slotsGrid: { @@ -491,138 +310,84 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', gap: 10, }, - slotButton: { - width: '31%', - height: 50, - backgroundColor: '#fff', + slotItem: { + width: '22.5%', + height: 44, borderRadius: 12, + backgroundColor: '#141420', alignItems: 'center', justifyContent: 'center', borderWidth: 1, - borderColor: '#e2e8f0', + borderColor: 'rgba(255,255,255,0.06)', }, slotActive: { - backgroundColor: '#1e293b', - borderColor: '#1e293b', + backgroundColor: '#6366f1', + borderColor: '#6366f1', }, slotBooked: { - backgroundColor: '#f1f5f9', - borderColor: '#f1f5f9', - opacity: 0.5, + opacity: 0.2, }, slotText: { + color: '#94a3b8', fontSize: 14, fontWeight: '700', - color: '#475569', }, slotTextActive: { color: '#fff', }, slotTextBooked: { textDecorationLine: 'line-through', - color: '#94a3b8', }, - emptyText: { - fontSize: 14, - color: '#94a3b8', - fontStyle: 'italic', - textAlign: 'center', - width: '100%', - padding: 20, - }, - waitlistCard: { - marginTop: 30, - padding: 20, - backgroundColor: '#fff', - borderRadius: 20, - borderWidth: 1, - borderColor: '#fee2e2', - alignItems: 'center', - }, - waitlistText: { - fontSize: 14, - color: '#b91c1c', - fontWeight: '600', - textAlign: 'center', - marginBottom: 16, - }, - // Step 4 - Resumo summaryCard: { padding: 20, - marginTop: 0, + gap: 12, }, summaryRow: { flexDirection: 'row', justifyContent: 'space-between', - marginBottom: 12, }, summaryLabel: { - fontSize: 14, color: '#64748b', + fontSize: 14, }, summaryValue: { + color: '#f8fafc', fontSize: 14, fontWeight: '700', - color: '#0f172a', - textAlign: 'right', - flex: 1, - marginLeft: 10, }, divider: { height: 1, - backgroundColor: '#e2e8f0', - marginVertical: 12, + backgroundColor: 'rgba(255,255,255,0.06)', + marginVertical: 4, }, summaryTotal: { + color: '#a5b4fc', fontSize: 22, fontWeight: '900', - color: '#6366f1', }, - notificationSettings: { - marginTop: 24, - }, - notifHelp: { - fontSize: 13, - color: '#64748b', - marginBottom: 16, - }, - reminderOptions: { + notifOptions: { flexDirection: 'row', - flexWrap: 'wrap', - gap: 8, + gap: 10, }, - notifButton: { - paddingHorizontal: 12, - paddingVertical: 10, + notifBtn: { + flex: 1, + paddingVertical: 12, borderRadius: 12, - backgroundColor: '#fff', + backgroundColor: '#141420', + alignItems: 'center', borderWidth: 1, - borderColor: '#e2e8f0', + borderColor: 'rgba(255,255,255,0.06)', }, - notifButtonActive: { - backgroundColor: '#6366f1', + notifBtnActive: { + backgroundColor: 'rgba(99,102,241,0.15)', borderColor: '#6366f1', }, - notifText: { - fontSize: 12, - fontWeight: 'bold', + notifBtnTxt: { color: '#64748b', + fontSize: 12, + fontWeight: '700', }, - notifTextActive: { - color: '#fff', - }, - // Footer - footer: { - padding: 16, - paddingBottom: 24, - backgroundColor: '#fff', - flexDirection: 'row', - gap: 12, - borderTopWidth: 1, - borderTopColor: '#e2e8f0', - }, - footerButton: { - flex: 1, + notifBtnTxtActive: { + color: '#a5b4fc', }, }); - diff --git a/src/pages/Cart.tsx b/src/pages/Cart.tsx index 1219872..9618295 100644 --- a/src/pages/Cart.tsx +++ b/src/pages/Cart.tsx @@ -1,55 +1,42 @@ import React from 'react'; -import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native'; +import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; 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'; export default function Cart() { const navigation = useNavigation(); - // Obtém o estado global do carrinho, e as funções comunicadoras do AppContext (interface BD) const { cart, shops, removeFromCart, placeOrder, user } = useApp(); - // Renderiza um estado/view vazia intercetiva, caso o array "cart" esteja vazio if (!cart.length) { return ( - - Sua Seleção está Deserta - + + 🛒 + Carrinho vazio + Adicione serviços ou produtos para começar. + + ); } - /** - * Lógica de Agrupamento de itens no Carrinho. - * A nível de negócio, como as encomendas (orders) são feitas por Barbearia (shopId), - * agrupamos para construir a interface dividida por Lojas caso de adicione itens mistos. - * @returns {Record} Dicionário indexado pelo shopId - */ const grouped = cart.reduce>((acc, item) => { acc[item.shopId] = acc[item.shopId] || []; acc[item.shopId].push(item); return acc; }, {}); - /** - * Acionado ao clicar em 'Finalizar pedido' para uma dada loja no JSX. - * Interliga através da função do Contexto à base de dados para materializar - * o agrupo de serviços selecionados e criar uma tupla na tabela de Pedidos/Marcações da db. - * @param {string} shopId - ID da loja que receberá o pedido final. - */ const handleCheckout = async (shopId: string) => { - // Verificamos de forma segura pelo objeto user se o authState (sessão Supabase) existe if (!user) { Alert.alert('Sessão Necessária', 'Inicie sessão para confirmar o seu pedido'); navigation.navigate('Login' as never); return; } - // Gera a inserção na API (insert em tabelas de orders / ordem e dependentes) const order = await placeOrder(user.id, shopId); if (order) { Alert.alert('Sucesso', 'Pedido criado com sucesso!'); @@ -59,17 +46,12 @@ export default function Cart() { }; return ( - // A página permite visibilidade escalonada num conteúdo flexível (ScrollView) - Minha Seleção + Carrinho - {/* Renderiza dinamicamente 1 Card de Checkout por Loja agrupada no objeto `grouped` */} {Object.entries(grouped).map(([shopId, items]) => { - // Mapeia o mock do objeto de loja baseado na primmary key `shopId` const shop = shops.find((s) => s.id === shopId); - - // Agregador quantitativo do array reduzindo o total financeiro calculado pelos preços da BD local const total = items.reduce((sum, i) => { const price = i.type === 'service' @@ -79,59 +61,57 @@ export default function Cart() { }, 0); return ( - // Engloba os items duma só loja - + - - {/* Consome o nome e morada do registo principal (Profile > Shop) na UI */} + + {(shop?.name || 'B').charAt(0)} + + {shop?.name ?? 'Barbearia'} {shop?.address} - {/* Apresenta o custo transformado visualmente (ex: R$ / €) */} - {currency(total)} - {/* Listagem linha a linha dos items (relacionados por foreign key 'refId') */} + + {items.map((i) => { - // JOIN via frontend para resgatar o nome natural referenciado no menu original da Lojas const ref = i.type === 'service' ? shop?.services.find((s) => s.id === i.refId) : shop?.products.find((p) => p.id === i.refId); + const price = ref?.price ?? 0; return ( - - {/* Condicionamento estruturado na UI, mostra Serviço vs Produto perante a tipagem DB iterada */} - {i.type === 'service' ? 'Serviço: ' : 'Produto: '} - {ref?.name ?? 'Item'} x{i.qty} - - - {/* Elimina de forma independente o registo não guardado da persistência AppContext/State */} - + + + + {i.type === 'service' ? 'Serviço' : 'Produto'} + + + {ref?.name ?? 'Item'} + Qtd: {i.qty} · {currency(price * i.qty)} + + removeFromCart(i.refId)} style={styles.removeBtn}> + + ); })} - {/* Renderização condicional no React para encaminhar fluxo para login se anónimo */} - {user ? ( - - ) : ( - - )} - + + + + Total + {currency(total)} + + + + ); })} @@ -142,63 +122,150 @@ export default function Cart() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8fafc', + backgroundColor: '#0a0a0f', }, content: { - padding: 16, + padding: 20, + gap: 20, + paddingBottom: 32, }, title: { - fontSize: 24, - fontWeight: 'bold', - color: '#0f172a', - marginBottom: 16, + fontSize: 32, + fontWeight: '900', + color: '#f8fafc', + marginBottom: 8, }, - emptyCard: { - padding: 32, + emptyState: { + flex: 1, alignItems: 'center', + justifyContent: 'center', + padding: 40, + gap: 12, + }, + emptyIcon: { + fontSize: 56, + marginBottom: 8, + }, + emptyTitle: { + color: '#f8fafc', + fontSize: 22, + fontWeight: '800', }, emptyText: { - fontSize: 16, color: '#64748b', + fontSize: 15, + textAlign: 'center', }, shopCard: { + backgroundColor: '#141420', + borderRadius: 24, + padding: 20, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', marginBottom: 16, }, shopHeader: { flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 12, + alignItems: 'center', + gap: 14, + }, + shopIcon: { + width: 48, + height: 48, + borderRadius: 16, + backgroundColor: 'rgba(99,102,241,0.12)', + alignItems: 'center', + justifyContent: 'center', + }, + shopIconText: { + color: '#818cf8', + fontSize: 18, + fontWeight: '900', }, shopName: { - fontSize: 16, - fontWeight: 'bold', - color: '#0f172a', + fontSize: 18, + fontWeight: '800', + color: '#f8fafc', }, shopAddress: { - fontSize: 12, - color: '#64748b', + fontSize: 13, + color: '#475569', + marginTop: 2, }, - total: { - fontSize: 18, - fontWeight: 'bold', - color: '#6366f1', + divider: { + height: 1, + backgroundColor: 'rgba(255,255,255,0.06)', + marginVertical: 16, }, item: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + }, + itemInfo: { + flex: 1, + gap: 4, + }, + typePill: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(99,102,241,0.1)', + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 3, + marginBottom: 4, + }, + typeText: { + color: '#818cf8', + fontSize: 10, + fontWeight: '800', + textTransform: 'uppercase', + }, + itemName: { + fontSize: 16, + fontWeight: '700', + color: '#f8fafc', + }, + itemQty: { + fontSize: 13, + color: '#64748b', + fontWeight: '500', + }, + removeBtn: { + width: 36, + height: 36, + borderRadius: 12, + backgroundColor: 'rgba(239,68,68,0.08)', + alignItems: 'center', + justifyContent: 'center', + marginLeft: 12, + }, + removeText: { + color: '#ef4444', + fontSize: 14, + fontWeight: '800', + }, + totalRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingVertical: 8, - borderBottomWidth: 1, - borderBottomColor: '#e2e8f0', + marginBottom: 20, }, - itemText: { + totalLabel: { + color: '#94a3b8', fontSize: 14, - color: '#64748b', - flex: 1, + fontWeight: '700', + textTransform: 'uppercase', + letterSpacing: 1, }, - checkoutButton: { - width: '100%', - marginTop: 12, + totalValue: { + color: '#a5b4fc', + fontSize: 26, + fontWeight: '900', + }, + checkoutBtn: { + backgroundColor: '#6366f1', + borderRadius: 16, + paddingVertical: 16, }, }); - diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 326732d..603410a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,639 +1,278 @@ -/** - * @file Dashboard.tsx - * @description Painel principal (Dashboard) destinado exclusivamente a utilizadores - * do tipo 'barbearia'. Permite a gestão integral do negócio: marcações, pedidos, - * serviços prestados, produtos e equipa (barbeiros). - */ -import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, Image } from 'react-native'; +import React, { useState, useMemo } from 'react'; +import { Alert, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, Image } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; 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 { Input } from '../components/ui/Input'; -import { Badge } from '../components/ui/Badge'; import { currency } from '../lib/format'; import { supabase } from '../lib/supabase'; +type Tab = 'agenda' | 'servicos' | 'produtos' | 'equipa' | 'perfil'; + +const statusColor: Record = { + pendente: '#6366f1', + confirmado: '#10b981', + concluido: '#10b981', + cancelado: '#ef4444', +}; + +const statusLabel: Record = { + pendente: 'Pendente', + confirmado: 'Confirmado', + concluido: 'Concluído', + cancelado: 'Cancelado', +}; + export default function Dashboard() { const navigation = useNavigation(); - // Resgata variáveis e ações modificadoras do Contexto (ponte para o backend/Supabase) const { user, shops, appointments, orders, + refreshShops, updateAppointmentStatus, updateOrderStatus, addService, + updateService, + deleteService, addProduct, - addBarber, updateProduct, deleteProduct, - deleteService, - deleteBarber, + addBarber, updateBarber, + deleteBarber, updateShopDetails, - logout, } = useApp(); - // Garante a entidade da barbearia atual baseada na Foreign Key armazenada no utilizador logado - const shop = shops.find((s) => s.id === user?.shopId); - const [activeTab, setActiveTab] = useState<'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings'>('overview'); + const shop = useMemo(() => shops.find((s) => s.id === user?.shopId), [shops, user?.shopId]); + const [activeTab, setActiveTab] = useState('agenda'); + const [loading, setLoading] = useState(false); - // Estados locais dos subformulários na página para Adicionar entidades - const [svcName, setSvcName] = useState(''); - const [svcPrice, setSvcPrice] = useState('50'); - const [svcDuration, setSvcDuration] = useState('30'); - const [prodName, setProdName] = useState(''); - const [prodPrice, setProdPrice] = useState('30'); - const [prodStock, setProdStock] = useState('10'); - const [barberName, setBarberName] = useState(''); - const [barberSpecs, setBarberSpecs] = useState(''); - const [barberSearchQuery, setBarberSearchQuery] = useState(''); - const [editShopName, setEditShopName] = useState(''); - const [editShopAddress, setEditShopAddress] = useState(''); - const [editImageUrl, setEditImageUrl] = useState(''); - const [editPaymentMethods, setEditPaymentMethods] = useState(''); - const [editPhone1, setEditPhone1] = useState(''); - const [editPhone2, setEditPhone2] = useState(''); - const [editWhatsapp, setEditWhatsapp] = useState(''); - const [editInstagram, setEditInstagram] = useState(''); - const [editFacebook, setEditFacebook] = useState(''); - const [editScheduleJson, setEditScheduleJson] = useState(''); - const [uploadingImage, setUploadingImage] = useState(false); + // Agenda Filters + const [dateFilter, setDateFilter] = useState(new Date().toISOString().split('T')[0]); + const shopAppointments = useMemo(() => { + return appointments + .filter((a) => a.shopId === shop?.id && a.date.startsWith(dateFilter)) + .sort((a, b) => a.date.localeCompare(b.date)); + }, [appointments, shop?.id, dateFilter]); - useEffect(() => { - if (!shop) return; - setEditShopName(shop.name || ''); - setEditShopAddress(shop.address || ''); - setEditImageUrl(shop.imageUrl || ''); - setEditPaymentMethods((shop.paymentMethods || ['Dinheiro', 'Cartão de Crédito', 'Cartão de Débito']).join(', ')); - setEditPhone1(shop.contacts?.phone1 || ''); - setEditPhone2(shop.contacts?.phone2 || ''); - setEditWhatsapp(shop.socialNetworks?.whatsapp || ''); - setEditInstagram(shop.socialNetworks?.instagram || ''); - setEditFacebook(shop.socialNetworks?.facebook || ''); - setEditScheduleJson(JSON.stringify(shop.schedule || [ - { 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' }, - { day: 'Quinta-feira', open: '09:00', close: '19:30' }, - { day: 'Sexta-feira', open: '09:00', close: '19:30' }, - { day: 'Sábado', open: '09:00', close: '19:00' }, - { day: 'Domingo', open: '', close: '', closed: true }, - ], null, 2)); - }, [shop?.id]); - - // Segurança de Bloqueio - Validação estrita do role do utilziador no componente - if (!user || user.role !== 'barbearia' || !shop) { - return ( - - A carregar dados da barbearia... - - ); - } - - // Consultas de agregação de uso local sobre as variações gerais do state (como um SELECT com WHERE) - const shopAppointments = appointments.filter((a) => a.shopId === shop.id); - const shopOrders = orders.filter((o) => o.shopId === shop.id); - const completedAppointments = shopAppointments.filter((a) => a.status === 'concluido'); - const activeAppointments = shopAppointments.filter((a) => a.status !== 'concluido'); - const productOrders = shopOrders.filter((o) => o.items.some((i) => i.type === 'product')); - const historyAppointments = shopAppointments.filter((a) => a.status === 'concluido' || a.status === 'cancelado'); - - // Métricas agregadas globais calculadas dinamicamente - const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0); - const lowStock = shop.products.filter((p) => p.stock <= 3); - - const addNewService = () => { - if (!svcName.trim()) return; - addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] }); - setSvcName(''); - setSvcPrice('50'); - setSvcDuration('30'); - Alert.alert('Sucesso', 'Serviço adicionado'); - }; - - const addNewProduct = () => { - if (!prodName.trim()) return; - addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 }); - setProdName(''); - setProdPrice('30'); - setProdStock('10'); - Alert.alert('Sucesso', 'Produto adicionado'); - }; - - const addNewBarber = () => { - if (!barberName.trim()) return; - addBarber(shop.id, { - name: barberName, - specialties: [], - schedule: [], - }); - setBarberName(''); - Alert.alert('Sucesso', 'Profissional adicionado'); - }; - - const updateProductStock = (productId: string, delta: number) => { - const product = shop.products.find((p) => p.id === productId); - if (!product) return; - const next = { ...product, stock: Math.max(0, product.stock + delta) }; - updateProduct(shop.id, next); - }; - - const saveSettings = async () => { - try { - let parsedSchedule = shop.schedule; - if (editScheduleJson.trim()) { - parsedSchedule = JSON.parse(editScheduleJson); - } - - await updateShopDetails(shop.id, { - name: editShopName.trim() || shop.name, - address: editShopAddress.trim(), - imageUrl: editImageUrl.trim() || undefined, - paymentMethods: editPaymentMethods.split(',').map((item) => item.trim()).filter(Boolean), - contacts: { - phone1: editPhone1.trim(), - phone2: editPhone2.trim(), - }, - socialNetworks: { - whatsapp: editWhatsapp.trim(), - instagram: editInstagram.trim(), - facebook: editFacebook.trim(), - }, - schedule: parsedSchedule, - }); - Alert.alert('Sucesso', 'Definições guardadas.'); - } catch (e: any) { - Alert.alert('Erro', e?.message || 'Não foi possível guardar as definições. Confirma o JSON do horário.'); - } - }; - - const pickImage = async () => { - const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); - if (!permission.granted) { - Alert.alert('Permissão necessária', 'Autoriza o acesso às fotos para carregar imagens.'); - return null; - } + // Form states + const [editingId, setEditingId] = useState(null); + const [formSvc, setFormSvc] = useState({ name: '', price: '', duration: '' }); + const [formProd, setFormProd] = useState({ name: '', price: '', stock: '' }); + const [formBarb, setFormBarb] = useState({ name: '', specialties: '' }); + const pickImage = async (target: 'shop' | 'barber', barberId?: string) => { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, - quality: 0.85, allowsEditing: true, - aspect: [4, 3], + aspect: [16, 9], + quality: 0.5, }); - if (result.canceled || !result.assets[0]) return null; - return result.assets[0]; - }; + if (!result.canceled && result.assets[0].uri) { + setLoading(true); + try { + const uri = result.assets[0].uri; + const fileExt = uri.split('.').pop(); + const fileName = `${Date.now()}.${fileExt}`; + const filePath = `shop-${shop?.id}/${fileName}`; - const uploadImage = async (asset: ImagePicker.ImagePickerAsset, folder: string) => { - const ext = asset.uri.split('.').pop()?.split('?')[0] || 'jpg'; - const filePath = `${folder}/${shop.id}-${Date.now()}.${ext}`; - const response = await fetch(asset.uri); - const blob = await response.blob(); + const formData = new FormData(); + formData.append('file', { + uri, + name: fileName, + type: `image/${fileExt}`, + } as any); - const { error } = await supabase.storage - .from('shops') - .upload(filePath, blob as any, { - contentType: asset.mimeType || blob.type || 'image/jpeg', - }); - if (error) throw error; + const { data: uploadData, error: uploadError } = await supabase.storage + .from('shop-images') + .upload(filePath, formData); - const { data } = supabase.storage.from('shops').getPublicUrl(filePath); - return data.publicUrl; - }; + if (uploadError) throw uploadError; - const uploadCoverImage = async () => { - const asset = await pickImage(); - if (!asset) return; - try { - setUploadingImage(true); - const publicUrl = await uploadImage(asset, 'covers'); - await updateShopDetails(shop.id, { imageUrl: publicUrl }); - setEditImageUrl(publicUrl); - Alert.alert('Sucesso', 'Foto de capa atualizada.'); - } catch (e: any) { - Alert.alert('Erro', e?.message || 'Não foi possível carregar a imagem.'); - } finally { - setUploadingImage(false); + const { data: { publicUrl } } = supabase.storage + .from('shop-images') + .getPublicUrl(filePath); + + if (target === 'shop') { + await updateShopDetails(shop!.id, { imageUrl: publicUrl }); + } else if (barberId) { + const b = shop?.barbers.find(x => x.id === barberId); + if (b) await updateBarber(shop!.id, { ...b, imageUrl: publicUrl }); + } + await refreshShops(); + } catch (e: any) { + Alert.alert('Erro no Upload', e.message); + } finally { + setLoading(false); + } } }; - const uploadBarberImage = async (barberId: string) => { - const barber = shop.barbers.find((item) => item.id === barberId); - if (!barber) return; - - const asset = await pickImage(); - if (!asset) return; - try { - setUploadingImage(true); - const publicUrl = await uploadImage(asset, 'barbers'); - await updateBarber(shop.id, { ...barber, imageUrl: publicUrl }); - Alert.alert('Sucesso', 'Foto do profissional atualizada.'); - } catch (e: any) { - Alert.alert('Erro', e?.message || 'Não foi possível carregar a imagem.'); - } finally { - setUploadingImage(false); - } - }; - - const tabs = [ - { id: 'overview', label: 'Estatísticas' }, - { id: 'appointments', label: 'Reservas' }, - { id: 'history', label: 'Histórico' }, - { id: 'orders', label: 'Pedidos' }, - { id: 'services', label: 'Serviços' }, - { id: 'products', label: 'Produtos' }, - { id: 'barbers', label: 'Equipa' }, - { id: 'settings', label: 'Definições' }, - ]; + if (!shop) return null; return ( + {/* Header Dashboard */} - {shop.name} - + + Olá, Parceiro + {shop.name} + + navigation.navigate('ProfileTab' as never)}> + {user?.name.charAt(0)} + - - {tabs.map((tab) => ( - setActiveTab(tab.id as any)} - > - - {tab.label} - - - ))} - + {/* Tabs Horizontais */} + + + {[ + ['agenda', 'Agenda'], + ['servicos', 'Serviços'], + ['produtos', 'Inventário'], + ['equipa', 'Equipa'], + ['perfil', 'Definições'], + ].map(([id, label]) => ( + setActiveTab(id as Tab)} + style={[styles.tabItem, activeTab === id && styles.tabItemActive]} + > + {label} + + ))} + + - - {activeTab === 'overview' && ( - - - - Receita Total - {currency(totalRevenue)} - - - Pendentes - {activeAppointments.length} - - - Concluídos - {completedAppointments.length} - - - Stock baixo - 0 && styles.statValueWarning]}> - {lowStock.length} - - + + {activeTab === 'agenda' && ( + + + Agenda do Dia + - - )} - - {activeTab === 'appointments' && ( - - {activeAppointments.length > 0 ? ( - activeAppointments.map((a) => { - const svc = shop.services.find((s) => s.id === a.serviceId); - const barber = shop.barbers.find((b) => b.id === a.barberId); - - const dateParts = a.date.split(' '); - const dateObj = new Date(dateParts[0]); - const time = dateParts[1] || ''; - const day = dateObj.getDate(); - const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']; - const month = monthNames[dateObj.getMonth()] || ''; + {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); return ( - - - {day} - {month} - - {time} + + + {appt.date.split(' ')[1]} + + + + + {svc?.name || 'Serviço'} + {currency(appt.total)} + + Profissional: {barb?.name} + + {appt.status === 'pendente' && ( + updateAppointmentStatus(appt.id, 'confirmado')} style={styles.confirmBtn}> + Confirmar + + )} + {appt.status !== 'cancelado' && appt.status !== 'concluido' && ( + updateAppointmentStatus(appt.id, 'cancelado')} style={styles.cancelBtn}> + Cancelar + + )} - - - - {svc?.name ?? 'Serviço'} - {barber?.name} · {currency(a.total)} - - - {a.status} - - - - - Alterar status: - - {['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => ( - - ))} - - - - + ); }) ) : ( - - 📅 - Nenhum agendamento ativo. + + Nenhum agendamento para este dia. )} )} - {activeTab === 'orders' && ( - - {productOrders.length > 0 ? ( - productOrders.map((o) => ( - - - - {currency(o.total)} - {new Date(o.createdAt).toLocaleString('pt-BR')} - - - {o.status} - - - - {['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => ( - - ))} - - - )) - ) : ( - - Nenhum pedido de produtos - - )} - - )} - - {activeTab === 'history' && ( - - Histórico de Agendamentos - {historyAppointments.length > 0 ? ( - historyAppointments.map((a) => { - const svc = shop.services.find((s) => s.id === a.serviceId); - const barber = shop.barbers.find((b) => b.id === a.barberId); - - const dateParts = a.date.split(' '); - const dateObj = new Date(dateParts[0]); - const time = dateParts[1] || ''; - const day = dateObj.getDate(); - const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']; - const month = monthNames[dateObj.getMonth()] || ''; - - return ( - - - {day} - {month} - - {time} - - - - - - {svc?.name ?? 'Serviço'} - {barber?.name ?? 'Barbeiro'} · {currency(a.total)} - - - {a.status === 'concluido' ? 'Concluído' : 'Cancelado'} - - - - - ); - }) - ) : ( - - 📅 - Ainda não há registos concluídos ou cancelados. - - )} - - )} - - {activeTab === 'services' && ( - - {shop.services.map((s) => ( + {activeTab === 'servicos' && ( + + Gerir Serviços + {shop.services.map(s => ( - - - {s.name} - Duração: {s.duration} min - - {currency(s.price)} + + {s.name} + {s.duration} min · {currency(s.price)} - + deleteService(shop.id, s.id)} style={styles.deleteIcon}> + + ))} + - Adicionar serviço - - - - + Novo Serviço + setFormSvc({...formSvc, name: t})} /> + + setFormSvc({...formSvc, price: t})} /> + setFormSvc({...formSvc, duration: t})} /> + + )} - {activeTab === 'products' && ( - - {lowStock.length > 0 && ( - - - ⚠️ Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'} - - - )} - {shop.products.map((p) => ( - - - - {p.name} - Stock: {p.stock} unidades - - {currency(p.price)} - - - - - + {activeTab === 'equipa' && ( + + Equipa + {shop.barbers.map(b => ( + + pickImage('barber', b.id)} style={styles.barberAvatar}> + {b.imageUrl ? : {b.name.charAt(0)}} + + + {b.name} + {b.specialties.join(', ')} + deleteBarber(shop.id, b.id)} style={styles.deleteIcon}> + + ))} - - Adicionar produto - - - - - )} - {activeTab === 'barbers' && ( - - Profissionais - - - - - - - {shop.barbers - .filter(b => b.name.toLowerCase().includes(barberSearchQuery.toLowerCase())) - .map((b) => ( - - - {b.imageUrl ? ( - - ) : ( - {b.name.charAt(0)} - )} - - - {b.name} - {b.specialties.join(', ') || 'Barbeiro'} - - - uploadBarberImage(b.id)} style={styles.photoBarberBtn} disabled={uploadingImage}> - Foto - - { - Alert.alert('Confirmar', 'Deseja remover este profissional?', [ - { text: 'Cancelar', style: 'cancel' }, - { text: 'Remover', style: 'destructive', onPress: () => deleteBarber(shop.id, b.id) }, - ]); - }} - style={styles.deleteBarberBtn} - > - Remover - - - - ))} - - {shop.barbers.filter(b => b.name.toLowerCase().includes(barberSearchQuery.toLowerCase())).length === 0 && ( - - Nenhum profissional encontrado. - + {activeTab === 'perfil' && ( + + Definições do Espaço + pickImage('shop')} style={styles.coverUpload}> + {shop.imageUrl ? ( + + ) : ( + Alterar Foto de Capa )} - - + 📷 + + - Adicionar profissional - - - - - )} - - {activeTab === 'settings' && ( - - - Definições da Barbearia - - - - {!!editImageUrl && } - - setEditPhone1(text.replace(/\D/g, '').slice(0, 9))} keyboardType="phone-pad" placeholder="910000000" /> - setEditPhone2(text.replace(/\D/g, '').slice(0, 9))} keyboardType="phone-pad" placeholder="252000000" /> - - - - - Horário em JSON. Mantém a estrutura day/open/close/closed. - - + Nome do Espaço + updateShopDetails(shop.id, { name: t })} /> + + Endereço + updateShopDetails(shop.id, { address: t })} /> + + )} @@ -645,322 +284,298 @@ export default function Dashboard() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8fafc', + backgroundColor: '#0a0a0f', }, header: { flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', - padding: 16, - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#e2e8f0', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 16, }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: '#0f172a', + headerInfo: { + flex: 1, + gap: 2, }, - tabsContainer: { - backgroundColor: '#fff', - borderBottomWidth: 1, - borderBottomColor: '#e2e8f0', + headerGreeting: { + color: '#64748b', + fontSize: 14, + fontWeight: '600', }, - tab: { - paddingHorizontal: 16, - paddingVertical: 12, + headerShopName: { + color: '#f8fafc', + fontSize: 24, + fontWeight: '900', + }, + brandPill: { + width: 44, + height: 44, + borderRadius: 14, + backgroundColor: '#141420', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(99,102,241,0.3)', + }, + brandText: { + color: '#818cf8', + fontSize: 18, + fontWeight: '900', + }, + tabContainer: { + backgroundColor: '#141420', + paddingVertical: 4, + }, + tabsScroll: { + paddingHorizontal: 20, + gap: 16, + }, + tabItem: { + paddingVertical: 14, borderBottomWidth: 2, borderBottomColor: 'transparent', }, - tabActive: { + tabItemActive: { borderBottomColor: '#6366f1', }, tabText: { + color: '#475569', fontSize: 14, - fontWeight: '600', - color: '#64748b', - }, - tabTextActive: { - color: '#6366f1', - }, - content: { - flex: 1, - }, - contentInner: { - padding: 16, - paddingBottom: 32, - }, - statsGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - marginBottom: 16, - }, - statCard: { - flex: 1, - minWidth: '45%', - padding: 16, - }, - statLabel: { - fontSize: 12, - color: '#64748b', - marginBottom: 4, - }, - statValue: { - fontSize: 20, - fontWeight: 'bold', - color: '#0f172a', - }, - statValueWarning: { - color: '#6366f1', - }, - itemCard: { - marginBottom: 12, - padding: 16, - }, - itemCardWarning: { - borderColor: '#c7d2fe', - backgroundColor: '#e0e7ff', - }, - itemHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 12, - }, - itemName: { - fontSize: 16, - fontWeight: 'bold', - color: '#0f172a', - flex: 1, - }, - itemDesc: { - fontSize: 14, - color: '#64748b', - marginTop: 4, - }, - itemPrice: { - fontSize: 18, - fontWeight: 'bold', - color: '#6366f1', - }, - statusSelector: { - marginTop: 8, - }, - selectorLabel: { - fontSize: 12, - color: '#64748b', - marginBottom: 8, - }, - statusButtons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 8, - }, - statusButton: { - flex: 1, - minWidth: '22%', - }, - emptyCard: { - padding: 32, - alignItems: 'center', - }, - emptyText: { - fontSize: 14, - color: '#64748b', - }, - alertCard: { - backgroundColor: '#e0e7ff', - borderColor: '#c7d2fe', - marginBottom: 16, - padding: 16, - }, - alertText: { - fontSize: 14, - fontWeight: '600', - color: '#4338ca', - }, - formCard: { - marginTop: 16, - padding: 16, - }, - formTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#0f172a', - marginBottom: 16, - }, - settingsHint: { - color: '#64748b', - fontSize: 12, - marginBottom: 8, - }, - scheduleInput: { - minHeight: 180, - textAlignVertical: 'top', - fontSize: 12, - lineHeight: 18, - }, - addButton: { - width: '100%', - marginTop: 8, - }, - deleteButton: { - width: '100%', - marginTop: 8, - }, - stockControls: { - flexDirection: 'row', - gap: 8, - marginTop: 8, - }, - stockButton: { - flex: 1, - }, - searchContainer: { - marginBottom: 8, - }, - searchInput: { - backgroundColor: '#fff', - borderColor: '#e2e8f0', - height: 50, - borderRadius: 12, - paddingHorizontal: 16, - color: '#0f172a', - fontSize: 14, - borderWidth: 1, - }, - barberList: { - gap: 12, - }, - barberCard: { - flexDirection: 'row', - alignItems: 'center', - padding: 12, - }, - barberAvatar: { - width: 50, - height: 50, - borderRadius: 25, - backgroundColor: '#f1f5f9', - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - borderWidth: 1, - borderColor: '#e2e8f0', - overflow: 'hidden', - }, - barberAvatarImage: { - width: '100%', - height: '100%', - }, - barberInfo: { - flex: 1, - }, - barberNameText: { - fontSize: 16, - fontWeight: 'bold', - color: '#0f172a', - }, - barberSpecialtyText: { - fontSize: 12, - color: '#64748b', - }, - barberActions: { - alignItems: 'flex-end', - gap: 6, - }, - photoBarberBtn: { - padding: 8, - }, - deleteBarberBtn: { - padding: 8, - }, - coverPreview: { - width: '100%', - height: 160, - borderRadius: 12, - marginBottom: 12, - }, - emptyResults: { - padding: 24, - alignItems: 'center', - }, - agendaContainer: { - gap: 12, - }, - agendaTicket: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 24, - overflow: 'hidden', - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.05, - shadowRadius: 10, - elevation: 3, - marginBottom: 8, - }, - agendaDateBox: { - backgroundColor: '#0f172a', - paddingVertical: 16, - paddingHorizontal: 12, - alignItems: 'center', - justifyContent: 'center', - minWidth: 85, - }, - agendaDay: { - color: '#fff', - fontSize: 28, - fontWeight: '900', - lineHeight: 32, - }, - agendaMonth: { - color: '#818cf8', - fontSize: 14, - fontWeight: '800', - textTransform: 'uppercase', - marginBottom: 8, - }, - agendaTimeWrapper: { - backgroundColor: 'rgba(255,255,255,0.1)', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - }, - agendaTime: { - color: '#fff', - fontSize: 12, fontWeight: '700', }, - agendaContent: { - flex: 1, - padding: 16, - justifyContent: 'center', + tabTextActive: { + color: '#f8fafc', + }, + content: { + padding: 20, + paddingBottom: 40, + }, + tabBox: { + gap: 20, }, agendaHeader: { flexDirection: 'row', justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 4, + alignItems: 'center', }, - agendaShopName: { - fontSize: 16, + sectionTitle: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '800', + }, + dateInput: { + backgroundColor: '#141420', + borderRadius: 10, + paddingHorizontal: 12, + paddingVertical: 6, + color: '#f8fafc', + fontSize: 13, + fontWeight: '700', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', + }, + agendaTicket: { + flexDirection: 'row', + padding: 0, + overflow: 'hidden', + height: 110, + }, + ticketSide: { + width: 70, + backgroundColor: '#1c1c2e', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + ticketTime: { + color: '#f8fafc', + fontSize: 18, fontWeight: '900', - color: '#0f172a', - marginRight: 8, }, - emptyAgendaState: { + ticketDot: { + width: 8, + height: 8, + borderRadius: 4, + }, + ticketMain: { + flex: 1, + padding: 16, + justifyContent: 'center', + }, + ticketRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + ticketSvc: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + ticketPrice: { + color: '#a5b4fc', + fontSize: 15, + fontWeight: '800', + }, + ticketMeta: { + color: '#64748b', + fontSize: 12, + marginTop: 4, + }, + ticketActions: { + flexDirection: 'row', + gap: 12, + marginTop: 12, + }, + confirmBtn: { + backgroundColor: 'rgba(16,185,129,0.15)', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 8, + }, + confirmBtnTxt: { + color: '#10b981', + fontSize: 11, + fontWeight: '800', + textTransform: 'uppercase', + }, + cancelBtn: { + backgroundColor: 'rgba(239,68,68,0.15)', + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 8, + }, + cancelBtnTxt: { + color: '#ef4444', + fontSize: 11, + fontWeight: '800', + textTransform: 'uppercase', + }, + emptyBox: { alignItems: 'center', padding: 40, - backgroundColor: '#fff', - borderRadius: 28, + backgroundColor: '#141420', + borderRadius: 20, borderWidth: 1, - borderColor: '#e2e8f0', + borderColor: 'rgba(255,255,255,0.06)', borderStyle: 'dashed', - gap: 16, }, - emptyAgendaIcon: { - fontSize: 48, + emptyTxt: { + color: '#475569', + fontSize: 14, + fontWeight: '600', + }, + itemCard: { + flexDirection: 'row', + alignItems: 'center', + gap: 14, + padding: 14, + }, + itemInfo: { + flex: 1, + }, + itemName: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + itemMeta: { + color: '#64748b', + fontSize: 13, + }, + deleteIcon: { + width: 32, + height: 32, + borderRadius: 10, + backgroundColor: 'rgba(239,68,68,0.1)', + alignItems: 'center', + justifyContent: 'center', + }, + deleteTxt: { + color: '#ef4444', + fontSize: 12, + fontWeight: '900', + }, + formCard: { + padding: 20, + gap: 12, + borderWidth: 1.5, + borderColor: 'rgba(99,102,241,0.2)', + }, + formTitle: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + marginBottom: 4, + }, + input: { + backgroundColor: '#1c1c2e', + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 12, + color: '#f8fafc', + fontSize: 14, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.04)', + }, + inputLabel: { + color: '#94a3b8', + fontSize: 12, + fontWeight: '700', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + row: { + flexDirection: 'row', + gap: 12, + }, + barberAvatar: { + width: 48, + height: 48, + borderRadius: 14, + backgroundColor: '#1c1c2e', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + avatarTxt: { + color: '#6366f1', + fontSize: 20, + fontWeight: '900', + }, + coverUpload: { + height: 180, + borderRadius: 22, + backgroundColor: '#141420', + overflow: 'hidden', + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', + }, + fullImg: { + width: '100%', + height: '100%', + }, + uploadOverlay: { + position: 'absolute', + bottom: 12, + right: 12, + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#6366f1', + alignItems: 'center', + justifyContent: 'center', + elevation: 4, + }, + uploadIcon: { + fontSize: 18, + }, + uploadTxt: { + color: '#475569', + fontSize: 14, + fontWeight: '700', }, }); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 19ae346..5d15a9c 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,21 +1,20 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Alert, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Alert, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, Image } 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 { Card } from '../components/ui/Card'; -import { Badge } from '../components/ui/Badge'; import { Button } from '../components/ui/Button'; import { currency } from '../lib/format'; import { RootStackParamList } from '../navigation/types'; import { supabase } from '../lib/supabase'; -const statusColor: Record = { - pendente: 'indigo', - confirmado: 'green', - concluido: 'green', - cancelado: 'red', +const statusColor: Record = { + pendente: '#6366f1', + confirmado: '#10b981', + concluido: '#10b981', + cancelado: '#ef4444', }; const statusLabel: Record = { @@ -41,7 +40,7 @@ export default function Profile() { submitReview, } = useApp(); - const [activeTab, setActiveTab] = useState('favoritos'); + const [activeTab, setActiveTab] = useState('agenda'); const [reviewedAppointments, setReviewedAppointments] = useState>(new Set()); const [reviewTarget, setReviewTarget] = useState<{ appointmentId: string; shopId: string; shopName: string } | null>(null); const [rating, setRating] = useState(0); @@ -100,19 +99,15 @@ export default function Profile() { ); } - const resetReview = () => { - setReviewTarget(null); - setRating(0); - setComment(''); - }; - const handleReviewSubmit = async () => { if (!reviewTarget || rating === 0) return; try { setSubmittingReview(true); await submitReview(reviewTarget.shopId, reviewTarget.appointmentId, rating, comment); setReviewedAppointments((prev) => new Set([...prev, reviewTarget.appointmentId])); - resetReview(); + setReviewTarget(null); + setRating(0); + setComment(''); Alert.alert('Obrigado', 'A tua avaliação foi enviada.'); } catch (e: any) { Alert.alert('Erro', e?.message || 'Erro ao enviar avaliação.'); @@ -123,205 +118,190 @@ export default function Profile() { return ( - - - + + {/* Header Perfil */} + + {user.name.charAt(0).toUpperCase()} - {user.name} - {user.email} - - {user.role === 'cliente' ? 'Cliente' : 'Barbearia'} - + {user.name} + {user.email} + + {user.role === 'cliente' ? 'Cliente' : 'Parceiro'} + - - - - - + + + + {/* Notificações (Horizontal) */} {myNotifications.length > 0 && ( - + Notificações - {myNotifications.map((notification) => ( - - {notification.read ? 'Notificação' : 'Nova vaga'} - {notification.message} - {!notification.read && ( - - )} - - ))} + + {myNotifications.map((n) => ( + + {n.message} + {!n.read && ( + markNotificationRead(n.id)}> + Marcar lida + + )} + + ))} + )} - {reviewTarget && ( - - Avaliar atendimento - {reviewTarget.shopName} - - {[1, 2, 3, 4, 5].map((star) => ( - setRating(star)} style={styles.starButton}> - - - ))} - - - - - - - - )} - - + {/* Tabs Estilizadas */} + {[ - ['favoritos', 'Favoritos', favoriteShops.length], - ['agenda', 'Agenda', myAppointments.length], - ['pedidos', 'Pedidos', myOrders.length], - ].map(([id, label, count]) => ( + ['agenda', 'Agenda'], + ['favoritos', 'Favoritos'], + ['pedidos', 'Pedidos'], + ].map(([id, label]) => ( setActiveTab(id as Tab)} + style={[styles.tabItem, activeTab === id && styles.tabActive]} > {label} - {count} + {activeTab === id && } ))} - {activeTab === 'favoritos' && ( - - Cofre de Favoritos - {favoriteShops.length ? favoriteShops.map((shop) => ( - navigation.navigate('ShopDetails', { shopId: shop.id })}> - - - {shop.name} - {shop.rating.toFixed(1)} - - {shop.address} - - - )) : ( - - Nenhuma barbearia favorita ainda. - - )} - - )} + {/* Conteúdo das Tabs */} + + {activeTab === 'agenda' && ( + + {myAppointments.length > 0 ? ( + myAppointments.map((appt) => { + const shop = shops.find((s) => s.id === appt.shopId); + const canReview = appt.status === 'concluido' && !reviewedAppointments.has(appt.id); + const dateParts = appt.date.split(' '); + const dateStr = dateParts[0]; + const timeStr = dateParts[1] || ''; - {activeTab === 'agenda' && ( - - Próximos Agendamentos - {myAppointments.length ? myAppointments.map((appointment) => { - const shop = shops.find((s) => s.id === appointment.shopId); - const service = shop?.services.find((s) => s.id === appointment.serviceId); - const canReview = appointment.status === 'concluido' && !reviewedAppointments.has(appointment.id); - - const dateParts = appointment.date.split(' '); - const dateObj = new Date(dateParts[0]); - const time = dateParts[1] || ''; - const day = dateObj.getDate(); - const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']; - const month = monthNames[dateObj.getMonth()] || ''; - - return ( - - - {day} - {month} - - {time} - - - - - {shop?.name || 'Barbearia'} - - {statusLabel[appointment.status]} - - - {!!service && {service.name}} - - {currency(appointment.total)} - {canReview ? ( - { - setReviewTarget({ - appointmentId: appointment.id, - shopId: appointment.shopId, - shopName: shop?.name || 'Barbearia', - }); - }} + return ( + + + + {dateStr.split('-')[2]} + {new Date(dateStr).toLocaleString('pt-PT', { month: 'short' }).toUpperCase()} + + + {shop?.name || 'Barbearia'} + {timeStr} · {currency(appt.total)} + + + {statusLabel[appt.status]} + + + {canReview && ( + + )} + + ); + }) + ) : ( + + Nenhum agendamento ativo. - ); - }) : ( - - 📅 - Não tens marcações agendadas. - - - )} - - )} + )} + + )} - {activeTab === 'pedidos' && ( - - Meus Pedidos - {myOrders.length ? myOrders.map((order) => { - const shop = shops.find((s) => s.id === order.shopId); - return ( - - - {shop?.name || 'Barbearia'} - {statusLabel[order.status]} - - {new Date(order.createdAt).toLocaleString('pt-PT')} - {order.items.length} item(s) - {currency(order.total)} - - ); - }) : ( - - Ainda não compraste produtos. - - )} - + {activeTab === 'favoritos' && ( + + {favoriteShops.length > 0 ? ( + favoriteShops.map((shop) => ( + navigation.navigate('ShopDetails', { shopId: shop.id })} + > + + + {shop.name.charAt(0)} + + + {shop.name} + {shop.address} + + ★ {shop.rating.toFixed(1)} + + + )) + ) : ( + + Sem barbearias favoritas. + + )} + + )} + + {activeTab === 'pedidos' && ( + + {myOrders.length > 0 ? ( + myOrders.map((order) => { + const shop = shops.find((s) => s.id === order.shopId); + return ( + + + {shop?.name || 'Barbearia'} + {currency(order.total)} + + + {new Date(order.createdAt).toLocaleDateString()} + + {statusLabel[order.status]} + + + + ); + }) + ) : ( + + Ainda não tens pedidos. + + )} + + )} + + + {/* Modal de Avaliação (Inline) */} + {reviewTarget && ( + + Como foi o serviço na {reviewTarget.shopName}? + + {[1, 2, 3, 4, 5].map((s) => ( + setRating(s)}> + = s && styles.starActive]}>★ + + ))} + + + + + + + )} @@ -331,330 +311,317 @@ export default function Profile() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8fafc', + backgroundColor: '#0a0a0f', }, content: { - padding: 16, + padding: 20, + paddingBottom: 100, + }, + profileHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 32, gap: 16, }, + avatarBox: { + width: 64, + height: 64, + borderRadius: 22, + backgroundColor: '#141420', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(99,102,241,0.3)', + }, + avatarText: { + color: '#818cf8', + fontSize: 24, + fontWeight: '900', + }, + profileInfo: { + flex: 1, + gap: 2, + }, + profileName: { + color: '#f8fafc', + fontSize: 22, + fontWeight: '900', + }, + profileEmail: { + color: '#64748b', + fontSize: 14, + fontWeight: '500', + }, + roleBadge: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(99,102,241,0.1)', + borderRadius: 8, + paddingHorizontal: 8, + paddingVertical: 3, + marginTop: 4, + }, + roleText: { + color: '#818cf8', + fontSize: 10, + fontWeight: '800', + textTransform: 'uppercase', + }, + logoutBtn: { + padding: 8, + }, + logoutIcon: { + color: '#ef4444', + fontSize: 24, + fontWeight: '700', + }, + sectionTitle: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '800', + marginBottom: 12, + }, + notifSection: { + marginBottom: 32, + }, + notifScroll: { + marginHorizontal: -20, + paddingHorizontal: 20, + }, + notifCard: { + backgroundColor: '#141420', + width: 240, + borderRadius: 16, + padding: 14, + marginRight: 12, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', + gap: 8, + }, + notifUnread: { + borderColor: 'rgba(99,102,241,0.4)', + }, + notifMsg: { + color: '#94a3b8', + fontSize: 13, + lineHeight: 18, + }, + markRead: { + color: '#818cf8', + fontSize: 12, + fontWeight: '700', + }, + tabBar: { + flexDirection: 'row', + marginBottom: 24, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255,255,255,0.06)', + }, + tabItem: { + paddingVertical: 12, + marginRight: 24, + position: 'relative', + }, + tabActive: {}, + tabText: { + color: '#475569', + fontSize: 15, + fontWeight: '700', + }, + tabTextActive: { + color: '#a5b4fc', + }, + tabIndicator: { + position: 'absolute', + bottom: -1, + left: 0, + right: 0, + height: 2, + backgroundColor: '#6366f1', + borderRadius: 2, + }, + tabContent: { + minHeight: 200, + }, + listContainer: { + gap: 12, + }, + agendaCard: { + padding: 14, + gap: 12, + }, + agendaTop: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + dateBox: { + backgroundColor: '#1c1c2e', + padding: 8, + borderRadius: 12, + alignItems: 'center', + minWidth: 50, + }, + dateDay: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '900', + }, + dateMonth: { + color: '#6366f1', + fontSize: 10, + fontWeight: '800', + }, + agendaMain: { + flex: 1, + gap: 2, + }, + agendaShop: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + agendaTime: { + color: '#64748b', + fontSize: 13, + fontWeight: '500', + }, + statusTag: { + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 10, + }, + statusText: { + fontSize: 11, + fontWeight: '800', + textTransform: 'uppercase', + }, + reviewBtn: { + marginTop: 4, + }, + shopCard: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + padding: 12, + }, + shopIcon: { + width: 44, + height: 44, + borderRadius: 12, + backgroundColor: 'rgba(255,255,255,0.03)', + alignItems: 'center', + justifyContent: 'center', + }, + shopIconText: { + color: '#475569', + fontSize: 18, + fontWeight: '800', + }, + shopName: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + shopAddr: { + color: '#475569', + fontSize: 12, + }, + shopRating: { + color: '#fbbf24', + fontWeight: '800', + fontSize: 13, + }, + orderCard: { + padding: 16, + gap: 8, + }, + orderHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + orderShop: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + orderPrice: { + color: '#a5b4fc', + fontSize: 16, + fontWeight: '900', + }, + orderFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + orderDate: { + color: '#475569', + fontSize: 12, + }, + emptyBox: { + alignItems: 'center', + paddingVertical: 40, + }, + emptyText: { + color: '#475569', + fontSize: 14, + fontWeight: '600', + }, + reviewModal: { + marginTop: 24, + padding: 20, + gap: 16, + borderColor: 'rgba(99,102,241,0.3)', + }, + reviewTitle: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + textAlign: 'center', + }, + stars: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + }, + star: { + fontSize: 32, + color: '#1c1c2e', + }, + starActive: { + color: '#fbbf24', + }, + reviewInput: { + backgroundColor: '#1c1c2e', + borderRadius: 14, + padding: 12, + color: '#f8fafc', + height: 80, + textAlignVertical: 'top', + }, + reviewActions: { + flexDirection: 'row', + gap: 12, + }, centerState: { flex: 1, alignItems: 'center', justifyContent: 'center', - padding: 24, - gap: 12, + padding: 40, + gap: 16, }, centerTitle: { - fontSize: 24, + color: '#f8fafc', + fontSize: 22, fontWeight: '900', - color: '#0f172a', - textAlign: 'center', }, centerText: { color: '#64748b', - fontSize: 14, + textAlign: 'center', lineHeight: 20, - fontWeight: '500', - }, - profileCard: { - backgroundColor: '#020617', - borderRadius: 28, - padding: 20, - flexDirection: 'row', - alignItems: 'center', - gap: 16, - }, - avatar: { - width: 72, - height: 72, - borderRadius: 20, - backgroundColor: 'rgba(255,255,255,0.1)', - alignItems: 'center', - justifyContent: 'center', - }, - avatarText: { - color: '#818cf8', - fontSize: 28, - fontWeight: '900', - }, - profileInfo: { - flex: 1, - }, - profileName: { - fontSize: 24, - fontWeight: '900', - color: '#fff', - }, - profileEmail: { - fontSize: 13, - color: '#94a3b8', - marginTop: 4, - }, - roleBadge: { - alignSelf: 'flex-start', - marginTop: 10, - }, - quickActions: { - flexDirection: 'row', - gap: 12, }, darkButton: { - flex: 1, - backgroundColor: '#0f172a', - }, - outlineButton: { - flex: 1, - }, - sectionTitle: { - fontSize: 20, - fontWeight: '900', - color: '#0f172a', - marginBottom: 12, - }, - itemCard: { - marginBottom: 12, - padding: 16, - }, - notificationUnread: { - borderColor: '#fecdd3', - borderWidth: 1, - }, - notificationTitle: { - color: '#e11d48', - fontSize: 15, - fontWeight: '900', - marginBottom: 8, - }, - itemHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - gap: 12, - marginBottom: 8, - }, - itemName: { - fontSize: 16, - fontWeight: '900', - color: '#0f172a', - flex: 1, - }, - itemDate: { - fontSize: 14, - color: '#64748b', - marginBottom: 5, - fontWeight: '500', - }, - itemTotal: { - fontSize: 18, - fontWeight: '900', - color: '#6366f1', - marginTop: 4, - }, - smallAction: { - marginTop: 12, - alignSelf: 'flex-start', - }, - tabs: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 20, - padding: 6, - gap: 6, - }, - tab: { - flex: 1, - borderRadius: 16, - paddingVertical: 11, - alignItems: 'center', - }, - tabActive: { - backgroundColor: '#0f172a', - }, - tabText: { - color: '#64748b', - fontSize: 11, - fontWeight: '900', - textTransform: 'uppercase', - }, - tabTextActive: { - color: '#818cf8', - }, - tabCount: { - color: '#94a3b8', - fontSize: 10, - fontWeight: '900', - marginTop: 2, - }, - ratingPill: { - color: '#fff', - backgroundColor: '#0f172a', - borderRadius: 999, - overflow: 'hidden', - paddingHorizontal: 10, - paddingVertical: 4, - fontSize: 12, - fontWeight: '900', - }, - emptyCard: { - padding: 32, - alignItems: 'center', - }, - emptyText: { - fontSize: 14, - color: '#64748b', - textAlign: 'center', - fontWeight: '600', - }, - reviewCard: { - padding: 18, - }, - stars: { - flexDirection: 'row', - justifyContent: 'center', - gap: 6, - marginVertical: 12, - }, - starButton: { - padding: 3, - }, - starText: { - color: '#cbd5e1', - fontSize: 34, - }, - starActive: { - color: '#818cf8', - }, - commentInput: { - minHeight: 86, - borderWidth: 1, - borderColor: '#e2e8f0', - borderRadius: 14, - padding: 12, - color: '#0f172a', - textAlignVertical: 'top', - fontWeight: '500', - }, - reviewActions: { - flexDirection: 'row', - gap: 10, - marginTop: 12, - }, - reviewButton: { - flex: 1, - }, - reviewedText: { - color: '#16a34a', - fontSize: 12, - fontWeight: '900', - textTransform: 'uppercase', - }, - agendaContainer: { - gap: 12, - }, - agendaTicket: { - flexDirection: 'row', - backgroundColor: '#fff', - borderRadius: 24, - overflow: 'hidden', - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.05, - shadowRadius: 10, - elevation: 3, - marginBottom: 8, - }, - agendaDateBox: { - backgroundColor: '#0f172a', - paddingVertical: 16, - paddingHorizontal: 12, - alignItems: 'center', - justifyContent: 'center', - minWidth: 85, - }, - agendaDay: { - color: '#fff', - fontSize: 28, - fontWeight: '900', - lineHeight: 32, - }, - agendaMonth: { - color: '#818cf8', - fontSize: 14, - fontWeight: '800', - textTransform: 'uppercase', - marginBottom: 8, - }, - agendaTimeWrapper: { - backgroundColor: 'rgba(255,255,255,0.1)', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - }, - agendaTime: { - color: '#fff', - fontSize: 12, - fontWeight: '700', - }, - agendaContent: { - flex: 1, - padding: 16, - justifyContent: 'center', - }, - agendaHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 4, - }, - agendaShopName: { - fontSize: 16, - fontWeight: '900', - color: '#0f172a', - flex: 1, - marginRight: 8, - }, - agendaBadge: { - transform: [{ scale: 0.9 }], - }, - agendaService: { - fontSize: 14, - color: '#64748b', - fontWeight: '600', - marginBottom: 12, - }, - agendaFooter: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - agendaTotal: { - fontSize: 16, - fontWeight: '900', - color: '#6366f1', - }, - reviewMiniButton: { - backgroundColor: '#818cf8', - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 12, - }, - reviewMiniButtonText: { - color: '#fff', - fontSize: 11, - fontWeight: '900', - textTransform: 'uppercase', - }, - emptyAgendaState: { - alignItems: 'center', - padding: 40, - backgroundColor: '#fff', - borderRadius: 28, - borderWidth: 1, - borderColor: '#e2e8f0', - borderStyle: 'dashed', - gap: 16, - }, - emptyAgendaIcon: { - fontSize: 48, + width: '100%', }, }); diff --git a/src/pages/ShopDetails.tsx b/src/pages/ShopDetails.tsx index 7a3a545..8d8c289 100644 --- a/src/pages/ShopDetails.tsx +++ b/src/pages/ShopDetails.tsx @@ -6,7 +6,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useApp } from '../context/AppContext'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { Badge } from '../components/ui/Badge'; import { currency } from '../lib/format'; import { RootStackParamList } from '../navigation/types'; @@ -34,7 +33,7 @@ export default function ShopDetails() { return ( - A carregar detalhes... + Carregando... ); @@ -44,11 +43,8 @@ export default function ShopDetails() { return ( - Barbearia não encontrada - O espaço pode ter sido removido ou o link está incorreto. - + Não encontrado + ); @@ -56,536 +52,402 @@ export default function ShopDetails() { const openMap = () => { const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${shop.name} ${shop.address}`)}`; - Linking.openURL(url).catch(() => Alert.alert('Erro', 'Não foi possível abrir o mapa.')); + Linking.openURL(url); }; const reserveService = (serviceId?: string) => { - if (!user) { - navigation.navigate('Login'); - return; - } + if (!user) { navigation.navigate('Login'); return; } navigation.navigate('Booking', { shopId: shop.id, serviceId }); }; const addProduct = (productId: string) => { - if (!user) { - navigation.navigate('Login'); - return; - } + if (!user) { navigation.navigate('Login'); return; } addToCart({ shopId: shop.id, type: 'product', refId: productId, qty: 1 }); - Alert.alert('Adicionado', 'Produto adicionado ao carrinho.'); + Alert.alert('Sucesso', 'Produto adicionado ao carrinho.'); }; const schedule = shop.schedule || defaultSchedule; - const paymentMethods = shop.paymentMethods || ['Dinheiro', 'Cartão de Crédito', 'Cartão de Débito']; - const contacts = shop.contacts || { phone1: '252 048 754', phone2: '252 048 754' }; const currentDayIndex = new Date().getDay() === 0 ? 6 : new Date().getDay() - 1; return ( - - + + + {/* Imagem Hero e Overlay */} {shop.imageUrl ? ( ) : ( - - SA - + 💈 )} - - - Mapa + + + navigation.goBack()} style={styles.backBtn}> + - toggleFavorite(shop.id)}> - - {isFavorite(shop.id) ? 'Favorito' : 'Guardar'} + toggleFavorite(shop.id)} style={styles.favBtn}> + + {isFavorite(shop.id) ? '♥' : '♡'} - - - - {(shop.rating || 0).toFixed(1)} Excelente + + + + + ★ {shop.rating.toFixed(1)} - {shop.name} - {shop.address} + {shop.name} + + 📍 {shop.address} + - - + {/* Tabs Estilizadas */} + + {[ ['servicos', 'Serviços'], - ['barbeiros', 'Barbeiros'], - ['produtos', 'Produtos'], - ['detalhes', 'Detalhes'], + ['barbeiros', 'Equipa'], + ['produtos', 'Loja'], + ['detalhes', 'Sobre'], ].map(([id, label]) => ( setTab(id as Tab)} - style={[styles.tab, tab === id && styles.tabActive]} + style={[styles.tabItem, tab === id && styles.tabItemActive]} > {label} ))} - {shop.services.length} serviços · {shop.barbers.length} barbeiros - {tab === 'servicos' && ( - - {shop.services.map((service) => ( - - - - {service.name} - {service.duration} min · Lugar disponível hoje + {/* Listagem de Conteúdo */} + + {tab === 'servicos' && ( + + {shop.services.map((s) => ( + + + {s.name} + {s.duration} min · {currency(s.price)} - {currency(service.price)} - - - - ))} - - )} + + + ))} + + )} - {tab === 'barbeiros' && ( - - - {shop.barbers.length === 0 ? ( - Esta barbearia ainda não registou barbeiros. - ) : shop.barbers.map((barber) => ( - - {barber.imageUrl ? ( - - ) : ( - - {barber.name.charAt(0).toUpperCase()} - - )} - {barber.name} - - {barber.specialties[0] || 'Especialista'} + {tab === 'barbeiros' && ( + + {shop.barbers.map((b) => ( + + + {b.name.charAt(0)} + + + {b.name} + {b.specialties.join(', ')} + + + ))} + + )} + + {tab === 'produtos' && ( + + {shop.products.map((p) => ( + + + {p.name} + {currency(p.price)} + + {p.stock} em stock + + + ))} + + )} + + {tab === 'detalhes' && ( + + Horário + {schedule.map((s, idx) => ( + + {s.day} + + {s.closed ? 'Fechado' : `${s.open} - ${s.close}`} ))} - - - )} - - {tab === 'produtos' && ( - - {shop.products.map((product) => { - const lowStock = product.stock <= 3; - return ( - - - P - {lowStock && Últimas} - - {product.name} - {product.stock} em stock - {currency(product.price)} - - - ); - })} - - )} - - {tab === 'detalhes' && ( - - Horário de atendimento - {schedule.map((slot, index) => ( - - - {slot.day} - {index === currentDayIndex && Hoje} - - {slot.closed ? 'Fechado' : `${slot.open} - ${slot.close}`} - - ))} - - - Formas de pagamento - - {paymentMethods.map((method) => ( - {method} - ))} - - - - Contacto - {[contacts.phone1, contacts.phone2].filter(Boolean).map((phone) => ( - Linking.openURL(`tel:${String(phone).replace(/\D/g, '')}`)}> - {phone} - Ligar + + Contacto + Linking.openURL(`tel:${shop.contacts?.phone1}`)}> + {shop.contacts?.phone1 || 'Não disponível'} - ))} - - )} + + )} + - + ); } const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f8fafc', + backgroundColor: '#0a0a0f', }, - content: { - padding: 16, - gap: 16, - }, - centerState: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: 24, - gap: 12, - }, - centerTitle: { - color: '#0f172a', - fontSize: 22, - fontWeight: '900', - textAlign: 'center', - }, - centerText: { - color: '#64748b', - textAlign: 'center', - fontWeight: '500', + scrollContent: { + paddingBottom: 40, }, hero: { - minHeight: 340, - borderRadius: 28, - overflow: 'hidden', - backgroundColor: '#0f172a', + height: 380, + position: 'relative', }, heroImage: { - ...StyleSheet.absoluteFillObject, width: '100%', height: '100%', }, - heroFallback: { - ...StyleSheet.absoluteFillObject, + heroPlaceholder: { + width: '100%', + height: '100%', + backgroundColor: '#141420', alignItems: 'center', justifyContent: 'center', }, - heroFallbackText: { - color: '#818cf8', - fontSize: 42, - fontWeight: '900', + heroPlaceholderText: { + fontSize: 64, }, heroOverlay: { ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(2,6,23,0.48)', + backgroundColor: 'rgba(10,10,15,0.5)', }, - heroActions: { + heroHeader: { position: 'absolute', - right: 14, - top: 14, + top: 0, + left: 0, + right: 0, flexDirection: 'row', - gap: 8, + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingTop: 10, }, - heroAction: { - backgroundColor: 'rgba(255,255,255,0.92)', - borderRadius: 14, - paddingHorizontal: 12, - paddingVertical: 9, + backBtn: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(255,255,255,0.1)', + alignItems: 'center', + justifyContent: 'center', }, - heroActionText: { - color: '#0f172a', - fontSize: 11, - fontWeight: '900', + backIcon: { + color: '#fff', + fontSize: 20, + fontWeight: '700', }, - favoriteActive: { - color: '#e11d48', + favBtn: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(255,255,255,0.1)', + alignItems: 'center', + justifyContent: 'center', }, - heroContent: { + favIcon: { + color: '#fff', + fontSize: 22, + }, + favActive: { + color: '#ef4444', + }, + heroBody: { position: 'absolute', - bottom: 22, - left: 18, - right: 18, - gap: 8, + bottom: 30, + left: 20, + right: 20, + gap: 10, }, - ratingPill: { + ratingBox: { alignSelf: 'flex-start', - backgroundColor: 'rgba(15,23,42,0.52)', - borderColor: 'rgba(255,255,255,0.18)', - borderWidth: 1, - borderRadius: 999, + backgroundColor: 'rgba(99,102,241,0.9)', + borderRadius: 8, paddingHorizontal: 10, - paddingVertical: 5, + paddingVertical: 4, }, - ratingText: { + ratingValue: { color: '#fff', - fontSize: 11, - fontWeight: '900', - textTransform: 'uppercase', - }, - title: { - color: '#fff', - fontSize: 34, - lineHeight: 36, - fontWeight: '900', - }, - address: { - color: 'rgba(255,255,255,0.9)', - fontSize: 15, - fontWeight: '600', - }, - tabShell: { - backgroundColor: 'rgba(255,255,255,0.72)', - borderRadius: 24, - padding: 8, - gap: 8, - }, - tabs: { - gap: 8, - }, - tab: { - borderRadius: 16, - paddingHorizontal: 16, - paddingVertical: 11, - backgroundColor: '#f1f5f9', - }, - tabActive: { - backgroundColor: '#0f172a', - }, - tabText: { - color: '#475569', fontSize: 12, fontWeight: '900', }, - tabTextActive: { - color: '#818cf8', - }, - summary: { - backgroundColor: '#fff', - color: '#94a3b8', - borderRadius: 16, - padding: 11, - fontSize: 11, + shopName: { + color: '#fff', + fontSize: 32, fontWeight: '900', - textTransform: 'uppercase', + }, + addrBox: { + opacity: 0.8, + }, + shopAddr: { + color: '#f8fafc', + fontSize: 14, + fontWeight: '500', + }, + tabSection: { + backgroundColor: '#141420', + paddingVertical: 6, + }, + tabsScroll: { + paddingHorizontal: 20, + gap: 16, + }, + tabItem: { + paddingVertical: 14, + paddingHorizontal: 4, + borderBottomWidth: 2, + borderBottomColor: 'transparent', + }, + tabItemActive: { + borderBottomColor: '#6366f1', + }, + tabText: { + color: '#475569', + fontSize: 14, + fontWeight: '700', + }, + tabTextActive: { + color: '#f8fafc', + }, + contentArea: { + padding: 20, }, grid: { gap: 12, }, serviceCard: { + flexDirection: 'row', + alignItems: 'center', padding: 16, + gap: 12, + }, + svcInfo: { + flex: 1, + gap: 4, + }, + svcName: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', + }, + svcMeta: { + color: '#64748b', + fontSize: 13, + }, + barberList: { + gap: 12, + }, + barberCard: { + flexDirection: 'row', + alignItems: 'center', + padding: 14, gap: 14, }, - itemTop: { - flexDirection: 'row', - alignItems: 'flex-start', - gap: 10, - }, - itemInfo: { - flex: 1, - gap: 6, - }, - itemTitle: { - color: '#0f172a', - fontSize: 18, - fontWeight: '900', - textTransform: 'uppercase', - }, - itemMeta: { - color: '#64748b', - fontSize: 12, - fontWeight: '700', - }, - price: { - color: '#0f172a', - backgroundColor: '#eef2ff', - borderRadius: 12, - paddingHorizontal: 10, - paddingVertical: 6, - fontSize: 16, - fontWeight: '900', - }, - reserveButton: { - backgroundColor: '#0f172a', - }, - panel: { - padding: 18, - }, - barberGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 16, - }, - barberItem: { - width: '29%', - minWidth: 92, - alignItems: 'center', - gap: 6, - }, - avatar: { - width: 74, - height: 74, - borderRadius: 999, - backgroundColor: '#e2e8f0', + barberAvatar: { + width: 50, + height: 50, + borderRadius: 16, + backgroundColor: '#1c1c2e', alignItems: 'center', justifyContent: 'center', }, - avatarImage: { - width: 74, - height: 74, - borderRadius: 999, - }, - avatarText: { - color: '#64748b', - fontSize: 24, + avatarTxt: { + color: '#6366f1', + fontSize: 20, fontWeight: '900', }, barberName: { - color: '#0f172a', - fontWeight: '900', - textAlign: 'center', + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', }, - barberSpecialty: { - color: '#94a3b8', + barberSpecs: { + color: '#475569', fontSize: 12, - textAlign: 'center', - fontWeight: '600', - }, - productGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, }, productCard: { - width: '48%', - padding: 12, + padding: 16, gap: 8, }, - productIcon: { - aspectRatio: 1, - borderRadius: 18, - backgroundColor: '#f8fafc', - alignItems: 'center', - justifyContent: 'center', - }, - productIconText: { - color: '#cbd5e1', - fontSize: 36, - fontWeight: '900', - }, - lowStock: { - position: 'absolute', - top: 8, - left: 8, - }, - productTitle: { - color: '#0f172a', - fontSize: 14, - fontWeight: '900', - textTransform: 'uppercase', - }, - productStock: { - color: '#94a3b8', - fontSize: 11, - fontWeight: '900', - textTransform: 'uppercase', - }, - productPrice: { - color: '#0f172a', - fontSize: 18, - fontWeight: '900', - }, - emptyText: { - color: '#64748b', - fontWeight: '600', - }, - detailTitle: { - color: '#0f172a', - fontSize: 19, - fontWeight: '900', - marginBottom: 12, - }, - scheduleRow: { + prodHeader: { flexDirection: 'row', justifyContent: 'space-between', - gap: 12, - paddingVertical: 7, + alignItems: 'flex-start', }, - scheduleDay: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, + prodName: { + color: '#f8fafc', + fontSize: 16, + fontWeight: '800', flex: 1, }, - scheduleDayText: { + prodPrice: { + color: '#a5b4fc', + fontSize: 16, + fontWeight: '900', + }, + prodStock: { + color: '#475569', + fontSize: 12, + marginBottom: 4, + }, + detailsBox: { + padding: 20, + gap: 12, + }, + detailTitle: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '800', + marginBottom: 8, + }, + schedRow: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + schedDay: { color: '#64748b', - fontWeight: '700', + fontSize: 14, }, - todayText: { - color: '#4f46e5', - fontWeight: '900', + schedTime: { + color: '#f8fafc', + fontSize: 14, + fontWeight: '600', }, - todayBadge: { - color: '#4338ca', - backgroundColor: '#e0e7ff', - borderRadius: 8, - paddingHorizontal: 7, - paddingVertical: 2, - fontSize: 10, - fontWeight: '900', - }, - scheduleTime: { - color: '#334155', + today: { + color: '#6366f1', fontWeight: '800', }, divider: { height: 1, - backgroundColor: '#e2e8f0', - marginVertical: 18, + backgroundColor: 'rgba(255,255,255,0.06)', + marginVertical: 12, }, - paymentList: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 8, - }, - paymentChip: { - backgroundColor: '#fff', - borderColor: '#e2e8f0', - borderWidth: 1, - color: '#334155', - borderRadius: 999, - paddingHorizontal: 12, - paddingVertical: 8, + contactLink: { + color: '#818cf8', + fontSize: 16, fontWeight: '700', }, - phoneCard: { - backgroundColor: '#fff', - borderColor: '#e2e8f0', - borderWidth: 1, - borderRadius: 16, - padding: 14, - flexDirection: 'row', + centerState: { + flex: 1, alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 10, + justifyContent: 'center', + padding: 40, }, - phoneText: { - color: '#334155', - fontSize: 16, - fontWeight: '900', - }, - phoneAction: { - color: '#4f46e5', - fontWeight: '900', + centerTitle: { + color: '#f8fafc', + fontSize: 18, + fontWeight: '800', }, });