457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
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;
|