From d29cf7535bc0997b78c1ed7fd5abdfefdfc303d7 Mon Sep 17 00:00:00 2001
From: 230417 <230417@epvc.pt>
Date: Tue, 12 May 2026 16:01:50 +0100
Subject: [PATCH] refactor: simplify booking flow and update status bar
appearance
---
App.tsx | 2 +-
src/pages/Booking.tsx | 741 +++++++--------------
src/pages/Cart.tsx | 255 +++++---
src/pages/Dashboard.tsx | 1309 +++++++++++++------------------------
src/pages/Profile.tsx | 963 +++++++++++++--------------
src/pages/ShopDetails.tsx | 696 ++++++++------------
6 files changed, 1621 insertions(+), 2345 deletions(-)
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 && (
+
- ) : appointment.status === 'concluido' ? (
- ✓ Avaliado
- ) : null}
-
-
+ Avaliar Experiência
+
+ )}
+
+ );
+ })
+ ) : (
+
+ 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',
},
});