first commit

This commit is contained in:
Rodrigo Nogueira de Sousa
2026-05-22 11:12:19 +01:00
commit 47d2cc65c3
91 changed files with 18528 additions and 0 deletions

View File

@@ -0,0 +1,456 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
ScrollView,
StyleSheet,
Pressable,
Alert,
RefreshControl,
} from 'react-native';
import { useAuth } from '../../context/AuthContext';
import { supabase, isSupabaseConfigured } from '../../services/supabase';
import LocalDataService from '../../services/localDataService';
import { COLORS, SIZES, FONTS, SHADOWS } from '../../constants/theme';
import { Booking, Barber, Service, User } from '../../types';
const AdminDashboardScreen: React.FC = () => {
const { user } = useAuth();
const [stats, setStats] = useState({
totalBookings: 0,
totalRevenue: 0,
activeBarbers: 0,
totalCustomers: 0,
});
const [recentBookings, setRecentBookings] = useState<Booking[]>([]);
const [barbers, setBarbers] = useState<Barber[]>([]);
const [services, setServices] = useState<Service[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
if (user?.role === 'admin') {
loadDashboardData();
}
}, [user]);
const loadDashboardData = async () => {
try {
if (isSupabaseConfigured) {
// Use Supabase if configured
const [bookingsRes, barbersRes, servicesRes, customersRes] = await Promise.all([
supabase
.from('bookings')
.select(`
*,
service:services(name, price),
customer:users(name),
barber:barbers(
user:users(name)
)
`)
.order('created_at', { ascending: false })
.limit(10),
supabase
.from('barbers')
.select(`
*,
user:users(name)
`)
.eq('is_active', true),
supabase
.from('services')
.select('*')
.eq('is_active', true),
supabase
.from('users')
.select('*')
.eq('role', 'customer')
]);
if (bookingsRes.data) {
const bookings = bookingsRes.data as Booking[];
setRecentBookings(bookings.slice(0, 5));
// Calculate stats
const completedBookings = bookings.filter(b => b.status === 'completed');
const totalRevenue = completedBookings.reduce((sum, b) => {
return sum + (b.service as any)?.price || 0;
}, 0);
setStats({
totalBookings: bookings.length,
totalRevenue,
activeBarbers: barbersRes.data?.length || 0,
totalCustomers: customersRes.data?.length || 0,
});
}
if (barbersRes.data) setBarbers(barbersRes.data);
if (servicesRes.data) setServices(servicesRes.data);
} else {
// Use local database
const [bookings, statsData, barbersData, servicesData] = await Promise.all([
LocalDataService.getBookings(),
LocalDataService.getStats(),
LocalDataService.getBarbers(),
LocalDataService.getServices(),
]);
if (bookings) {
setRecentBookings(bookings.slice(0, 5));
}
if (statsData) {
setStats(statsData);
}
if (barbersData) {
setBarbers(barbersData);
}
if (servicesData) {
setServices(servicesData);
}
}
} catch (error) {
console.error('Erro ao carregar dados do painel:', error);
Alert.alert('Erro', 'Falha ao carregar dados do painel');
} finally {
setLoading(false);
setRefreshing(false);
}
};
const onRefresh = () => {
setRefreshing(true);
loadDashboardData();
};
const handleUpdateBookingStatus = async (bookingId: string, status: string) => {
try {
if (isSupabaseConfigured) {
const { error } = await supabase
.from('bookings')
.update({ status })
.eq('id', bookingId);
if (error) throw error;
} else {
const result = await LocalDataService.updateBookingStatus(bookingId, status as Booking['status']);
if (!result) throw new Error('Falha ao actualizar marcação');
}
Alert.alert('Sucesso', `Marcação ${status} com sucesso`);
loadDashboardData();
} catch (error: any) {
Alert.alert('Erro', error.message || 'Falha ao actualizar marcação');
}
};
const StatCard = ({ title, value, subtitle }: { title: string; value: string | number; subtitle?: string }) => (
<View style={styles.statCard}>
<Text style={styles.statTitle}>{title}</Text>
<Text style={styles.statValue}>{value}</Text>
{subtitle && <Text style={styles.statSubtitle}>{subtitle}</Text>}
</View>
);
const BookingItem = ({ booking }: { booking: Booking }) => (
<View style={styles.bookingItem}>
<View style={styles.bookingHeader}>
<Text style={styles.bookingService}>{booking.service?.name}</Text>
<View style={[styles.statusBadge, {
backgroundColor: booking.status === 'completed' ? COLORS.success :
booking.status === 'confirmed' ? COLORS.primary :
booking.status === 'pending' ? COLORS.warning :
booking.status === 'cancelled' ? COLORS.error : COLORS.textSecondary
}]}>
<Text style={styles.statusText}>{booking.status}</Text>
</View>
</View>
<Text style={styles.bookingCustomer}>Cliente: {booking.customer?.name}</Text>
<Text style={styles.bookingBarber}>Barbeiro: {booking.barber?.user?.name}</Text>
<Text style={styles.bookingDate}>
{new Date(booking.booking_date).toLocaleDateString('pt-PT')} às {booking.booking_time}
</Text>
<Text style={styles.bookingPrice}>{booking.service?.price}</Text>
{booking.status === 'pending' && (
<View style={styles.bookingActions}>
<Pressable
style={({ pressed }) => [
styles.actionButton,
styles.confirmButton,
pressed && styles.actionButtonPressed
]}
onPress={() => handleUpdateBookingStatus(booking.id, 'confirmed')}
>
<Text style={styles.actionButtonText}>Confirmar</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.actionButton,
styles.cancelButton,
pressed && styles.actionButtonPressed
]}
onPress={() => handleUpdateBookingStatus(booking.id, 'cancelled')}
>
<Text style={styles.actionButtonText}>Cancelar</Text>
</Pressable>
</View>
)}
</View>
);
if (user?.role !== 'admin') {
return (
<View style={styles.accessDenied}>
<Text style={styles.accessDeniedText}>Acesso Negado</Text>
<Text style={styles.accessDeniedSubtext}>Requer acesso de administrador</Text>
</View>
);
}
if (loading) {
return (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>A carregar painel...</Text>
</View>
);
}
return (
<ScrollView
style={styles.container}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={COLORS.primary} />
}
>
<Text style={styles.title}>Painel de Administração</Text>
{/* Stats Cards */}
<View style={styles.statsContainer}>
<StatCard title="Total de Marcações" value={stats.totalBookings} />
<StatCard title="Receita Total" value={`${stats.totalRevenue}`} />
<StatCard title="Barbeiros Activos" value={stats.activeBarbers} />
<StatCard title="Total de Clientes" value={stats.totalCustomers} />
</View>
{/* Recent Bookings */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Marcações Recentes</Text>
{recentBookings.length === 0 ? (
<Text style={styles.emptyText}>Sem marcações recentes</Text>
) : (
recentBookings.map((booking) => (
<BookingItem key={booking.id} booking={booking} />
))
)}
</View>
{/* Quick Actions */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Acções Rápidas</Text>
<View style={styles.actionsGrid}>
<Pressable style={({ pressed }) => [styles.quickActionButton, pressed && styles.quickActionButtonPressed]}>
<Text style={styles.quickActionText}>Gerir Serviços</Text>
</Pressable>
<Pressable style={({ pressed }) => [styles.quickActionButton, pressed && styles.quickActionButtonPressed]}>
<Text style={styles.quickActionText}>Gerir Barbeiros</Text>
</Pressable>
<Pressable style={({ pressed }) => [styles.quickActionButton, pressed && styles.quickActionButtonPressed]}>
<Text style={styles.quickActionText}>Ver Relatórios</Text>
</Pressable>
<Pressable style={({ pressed }) => [styles.quickActionButton, pressed && styles.quickActionButtonPressed]}>
<Text style={styles.quickActionText}>Definições</Text>
</Pressable>
</View>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
...FONTS.body,
color: COLORS.textSecondary,
},
accessDenied: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
accessDeniedText: {
...FONTS.h2,
color: COLORS.error,
marginBottom: SIZES.base,
},
accessDeniedSubtext: {
...FONTS.body,
color: COLORS.textSecondary,
},
title: {
...FONTS.h1,
color: COLORS.text,
textAlign: 'center',
padding: SIZES.padding,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
statsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
padding: SIZES.margin,
},
statCard: {
width: '48%',
backgroundColor: COLORS.surface,
borderRadius: SIZES.radius,
padding: SIZES.padding,
marginBottom: SIZES.margin,
alignItems: 'center',
...SHADOWS.medium,
},
statTitle: {
...FONTS.caption,
color: COLORS.textSecondary,
marginBottom: SIZES.base / 2,
},
statValue: {
...FONTS.h2,
color: COLORS.primary,
fontWeight: 'bold',
},
statSubtitle: {
...FONTS.caption,
color: COLORS.textSecondary,
marginTop: SIZES.base / 2,
},
section: {
margin: SIZES.margin,
},
sectionTitle: {
...FONTS.h2,
color: COLORS.text,
marginBottom: SIZES.margin,
},
emptyText: {
...FONTS.body,
color: COLORS.textSecondary,
textAlign: 'center',
paddingVertical: SIZES.padding,
},
bookingItem: {
backgroundColor: COLORS.surface,
borderRadius: SIZES.radius,
padding: SIZES.padding,
marginBottom: SIZES.margin,
...SHADOWS.light,
},
bookingHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SIZES.margin,
},
bookingService: {
...FONTS.h3,
color: COLORS.text,
flex: 1,
},
statusBadge: {
paddingHorizontal: SIZES.base,
paddingVertical: SIZES.base / 2,
borderRadius: SIZES.base / 2,
},
statusText: {
...FONTS.caption,
color: COLORS.background,
fontWeight: 'bold',
},
bookingCustomer: {
...FONTS.body,
color: COLORS.textSecondary,
marginBottom: SIZES.base / 2,
},
bookingBarber: {
...FONTS.body,
color: COLORS.textSecondary,
marginBottom: SIZES.base / 2,
},
bookingDate: {
...FONTS.caption,
color: COLORS.textSecondary,
marginBottom: SIZES.base / 2,
},
bookingPrice: {
...FONTS.h3,
color: COLORS.primary,
fontWeight: 'bold',
marginBottom: SIZES.margin,
},
bookingActions: {
flexDirection: 'row',
justifyContent: 'space-between',
},
actionButton: {
borderRadius: SIZES.base,
paddingVertical: SIZES.base,
paddingHorizontal: SIZES.padding,
flex: 1,
marginHorizontal: SIZES.base / 2,
alignItems: 'center',
cursor: 'pointer',
},
actionButtonPressed: {
opacity: 0.8,
},
confirmButton: {
backgroundColor: COLORS.success,
},
cancelButton: {
backgroundColor: COLORS.error,
},
actionButtonText: {
...FONTS.caption,
color: COLORS.background,
fontWeight: 'bold',
},
actionsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
quickActionButton: {
width: '48%',
backgroundColor: COLORS.surface,
borderRadius: SIZES.radius,
padding: SIZES.padding,
marginBottom: SIZES.margin,
alignItems: 'center',
...SHADOWS.light,
cursor: 'pointer',
},
quickActionButtonPressed: {
opacity: 0.9,
},
quickActionText: {
...FONTS.body,
color: COLORS.text,
textAlign: 'center',
},
});
export default AdminDashboardScreen;