first commit
This commit is contained in:
456
src/screens/admin/AdminDashboardScreen.tsx
Normal file
456
src/screens/admin/AdminDashboardScreen.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user