Files
TravelMate/lib/components/home/calendar/calendar_page.dart

488 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
import '../../../models/trip.dart';
import '../../../models/activity.dart';
import '../../../blocs/activity/activity_bloc.dart';
import '../../../blocs/activity/activity_state.dart';
import '../../../blocs/activity/activity_event.dart';
import '../../../repositories/user_repository.dart';
import '../../../models/user.dart';
class CalendarPage extends StatefulWidget {
final Trip trip;
const CalendarPage({super.key, required this.trip});
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
late DateTime _focusedDay;
DateTime? _selectedDay;
final CalendarFormat _calendarFormat = CalendarFormat.week;
@override
void initState() {
super.initState();
_focusedDay = widget.trip.startDate;
_selectedDay = _focusedDay;
}
List<Activity> _getActivitiesForDay(DateTime day, List<Activity> activities) {
return activities.where((activity) {
if (activity.date == null) return false;
return isSameDay(activity.date, day);
}).toList();
}
Future<void> _selectTimeAndSchedule(Activity activity, DateTime date) async {
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
child: child!,
);
},
);
if (pickedTime != null && mounted) {
final scheduledDate = DateTime(
date.year,
date.month,
date.day,
pickedTime.hour,
pickedTime.minute,
);
context.read<ActivityBloc>().add(
UpdateActivityDate(
tripId: widget.trip.id!,
activityId: activity.id,
date: scheduledDate,
),
);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode
? theme.scaffoldBackgroundColor
: Colors.white,
appBar: AppBar(
title: Text(
widget.trip.title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios, color: theme.colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: Icon(Icons.people, color: theme.colorScheme.onSurface),
onPressed: () => _showParticipantsDialog(context),
),
],
),
body: BlocBuilder<ActivityBloc, ActivityState>(
builder: (context, state) {
if (state is ActivityLoading) {
return const Center(child: CircularProgressIndicator());
}
List<Activity> allActivities = [];
if (state is ActivityLoaded) {
allActivities = state.activities;
} else if (state is ActivitySearchResults) {
// Fallback if we are in search state
}
// Filter approved activities
final approvedActivities = allActivities.where((a) {
return a.isApprovedByAllParticipants([
...widget.trip.participants,
widget.trip.createdBy,
]);
}).toList();
final scheduledActivities = approvedActivities
.where((a) => a.date != null)
.toList();
final unscheduledActivities = approvedActivities
.where((a) => a.date == null)
.toList();
final selectedActivities = _getActivitiesForDay(
_selectedDay ?? _focusedDay,
scheduledActivities,
);
// Sort by time
selectedActivities.sort((a, b) => a.date!.compareTo(b.date!));
return Column(
children: [
// Calendar Strip
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isDarkMode ? theme.cardColor : Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: TableCalendar(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: theme.textTheme.titleLarge!.copyWith(
fontWeight: FontWeight.bold,
),
leftChevronIcon: const Icon(Icons.chevron_left),
rightChevronIcon: const Icon(Icons.chevron_right),
),
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
selectedDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
markerDecoration: const BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
),
),
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
eventLoader: (day) {
return _getActivitiesForDay(day, scheduledActivities);
},
),
),
const SizedBox(height: 16),
// Timeline View
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Timeline
if (selectedActivities.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
child: Text(
'Aucune activité prévue ce jour',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.5,
),
),
),
),
)
else
...selectedActivities.map((activity) {
return _buildTimelineItem(activity, theme);
}),
const SizedBox(height: 32),
// Unscheduled Activities Section
Text(
'Activités à ajouter',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (unscheduledActivities.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
'Toutes les activités sont planifiées !',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.5,
),
),
),
)
else
...unscheduledActivities.map((activity) {
return _buildUnscheduledActivityCard(activity, theme);
}),
const SizedBox(height: 32),
],
),
),
),
],
);
},
),
);
}
Widget _buildTimelineItem(Activity activity, ThemeData theme) {
final timeFormat = DateFormat('HH:mm'); // 10:00
final endTimeFormat = DateFormat('HH:mm'); // 11:30 (simulated duration)
// Simulate duration (1h30)
final endTime = activity.date!.add(const Duration(hours: 1, minutes: 30));
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Time Column
SizedBox(
width: 50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${activity.date!.hour}h',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
// Timeline Line
// Expanded(child: Container()), // Placeholder for line if needed
// Activity Card
Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 24),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getCategoryColor(
activity.category,
).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border(
left: BorderSide(
color: _getCategoryColor(activity.category),
width: 4,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'${timeFormat.format(activity.date!)} - ${endTimeFormat.format(endTime)}',
style: theme.textTheme.bodyMedium?.copyWith(
color: _getCategoryColor(activity.category),
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
);
}
Widget _buildUnscheduledActivityCard(Activity activity, ThemeData theme) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getCategoryColor(activity.category).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
_getCategoryIcon(activity.category),
color: _getCategoryColor(activity.category),
),
),
title: Text(
activity.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
activity.category,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
trailing: IconButton(
icon: const Icon(Icons.grid_view), // Drag handle icon
onPressed: () {
if (_selectedDay != null) {
_selectTimeAndSchedule(activity, _selectedDay!);
}
},
),
),
);
}
Color _getCategoryColor(String category) {
// Simple mapping based on category name
// You might want to use the enum if possible, but category is String in Activity model
if (category.toLowerCase().contains('musée') ||
category.toLowerCase().contains('museum')) {
return Colors.blue;
}
if (category.toLowerCase().contains('restaurant') ||
category.toLowerCase().contains('food')) {
return Colors.orange;
}
if (category.toLowerCase().contains('nature') ||
category.toLowerCase().contains('park')) {
return Colors.green;
}
if (category.toLowerCase().contains('photo') ||
category.toLowerCase().contains('attraction')) {
return Colors.purple;
}
if (category.toLowerCase().contains('détente') ||
category.toLowerCase().contains('relax')) {
return Colors.pink;
}
return Colors.teal;
}
IconData _getCategoryIcon(String category) {
if (category.toLowerCase().contains('musée')) return Icons.museum;
if (category.toLowerCase().contains('restaurant')) return Icons.restaurant;
if (category.toLowerCase().contains('nature')) return Icons.nature;
if (category.toLowerCase().contains('photo')) return Icons.camera_alt;
if (category.toLowerCase().contains('détente')) {
return Icons.icecream; // Gelato icon :)
}
return Icons.place;
}
void _showParticipantsDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Participants'),
content: SizedBox(
width: double.maxFinite,
child: FutureBuilder<List<User>>(
future: UserRepository().getUsersByIds([
...widget.trip.participants,
widget.trip.createdBy,
]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Erreur de chargement'));
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(child: Text('Aucun participant trouvé'));
}
return ListView.builder(
shrinkWrap: true,
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
final isCreator = user.id == widget.trip.createdBy;
return ListTile(
leading: CircleAvatar(
backgroundImage: user.profilePictureUrl != null
? NetworkImage(user.profilePictureUrl!)
: null,
child: user.profilePictureUrl == null
? Text(
'${user.prenom.isNotEmpty ? user.prenom[0] : ''}${user.nom.isNotEmpty ? user.nom[0] : ''}'
.toUpperCase(),
)
: null,
),
title: Text(user.fullName),
subtitle: isCreator ? const Text('Organisateur') : null,
);
},
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
),
);
}
}