feat: upgrade React Navigation to v7 and redesign appointment cards in Dashboard and Profile pages
This commit is contained in:
@@ -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 há 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user