feat: upgrade React Navigation to v7 and redesign appointment cards in Dashboard and Profile pages

This commit is contained in:
2026-05-06 12:44:35 +01:00
parent 99fc0a3882
commit 8f5a88788c
4 changed files with 412 additions and 144 deletions

View File

@@ -303,45 +303,64 @@ export default function Dashboard() {
)}
{activeTab === 'appointments' && (
<View>
<View style={styles.agendaContainer}>
{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()] || '';
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {a.date}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'indigo' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
<View key={a.id} style={styles.agendaTicket}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
</View>
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<View style={{ flex: 1 }}>
<Text style={styles.agendaShopName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {currency(a.total)}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'indigo' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</View>
</View>
</View>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ativo</Text>
</Card>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Nenhum agendamento ativo.</Text>
</View>
)}
</View>
)}
@@ -384,31 +403,48 @@ export default function Dashboard() {
)}
{activeTab === 'history' && (
<View>
<View style={styles.agendaContainer}>
<Text style={[styles.title, { marginBottom: 12 }]}>Histórico de Agendamentos</Text>
{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 (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name ?? 'Barbeiro'} · {a.date}</Text>
<Text style={styles.itemDesc}>{currency(a.total)}</Text>
<View key={a.id} style={[styles.agendaTicket, { opacity: 0.8 }]}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
<Badge color={a.status === 'concluido' ? 'green' : 'red'}>
{a.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</View>
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<View style={{ flex: 1 }}>
<Text style={styles.agendaShopName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name ?? 'Barbeiro'} · {currency(a.total)}</Text>
</View>
<Badge color={a.status === 'concluido' ? 'green' : 'red'}>
{a.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</View>
</View>
</View>
);
})
) : (
<Card style={styles.emptyCard}>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Ainda não registos concluídos ou cancelados.</Text>
</Card>
</View>
)}
</View>
)}
@@ -850,4 +886,81 @@ const styles = StyleSheet.create({
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',
},
agendaHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 4,
},
agendaShopName: {
fontSize: 16,
fontWeight: '900',
color: '#0f172a',
marginRight: 8,
},
emptyAgendaState: {
alignItems: 'center',
padding: 40,
backgroundColor: '#fff',
borderRadius: 28,
borderWidth: 1,
borderColor: '#e2e8f0',
borderStyle: 'dashed',
gap: 16,
},
emptyAgendaIcon: {
fontSize: 48,
},
});

View File

@@ -232,44 +232,70 @@ export default function Profile() {
)}
{activeTab === 'agenda' && (
<View>
<Text style={styles.sectionTitle}>Minha Agenda</Text>
<View style={styles.agendaContainer}>
<Text style={styles.sectionTitle}>Próximos Agendamentos</Text>
{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 (
<Card key={appointment.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name || 'Barbearia'}</Text>
<Badge color={statusColor[appointment.status]}>{statusLabel[appointment.status]}</Badge>
<View key={appointment.id} style={styles.agendaTicket}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
</View>
<Text style={styles.itemDate}>{appointment.date}</Text>
{!!service && <Text style={styles.itemDate}>{service.name} · {service.duration} min</Text>}
<Text style={styles.itemTotal}>{currency(appointment.total)}</Text>
{canReview ? (
<Button
style={styles.smallAction}
onPress={() => {
setReviewTarget({
appointmentId: appointment.id,
shopId: appointment.shopId,
shopName: shop?.name || 'Barbearia',
});
}}
>
Avaliar agora
</Button>
) : appointment.status === 'concluido' ? (
<Text style={styles.reviewedText}>Avaliado</Text>
) : null}
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<Text style={styles.agendaShopName}>{shop?.name || 'Barbearia'}</Text>
<Badge color={statusColor[appointment.status]} style={styles.agendaBadge}>
{statusLabel[appointment.status]}
</Badge>
</View>
{!!service && <Text style={styles.agendaService}>{service.name}</Text>}
<View style={styles.agendaFooter}>
<Text style={styles.agendaTotal}>{currency(appointment.total)}</Text>
{canReview ? (
<TouchableOpacity
style={styles.reviewMiniButton}
onPress={() => {
setReviewTarget({
appointmentId: appointment.id,
shopId: appointment.shopId,
shopName: shop?.name || 'Barbearia',
});
}}
>
<Text style={styles.reviewMiniButtonText}>Avaliar</Text>
</TouchableOpacity>
) : appointment.status === 'concluido' ? (
<Text style={styles.reviewedText}> Avaliado</Text>
) : null}
</View>
</View>
</View>
);
}) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Sem agendamentos futuros.</Text>
</Card>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Não tens marcações agendadas.</Text>
<Button
style={styles.darkButton}
onPress={() => navigation.navigate('Explore')}
>
Procurar Barbearias
</Button>
</View>
)}
</View>
)}
@@ -521,6 +547,114 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: '900',
textTransform: 'uppercase',
marginTop: 10,
},
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,
},
});