417 lines
14 KiB
Dart
417 lines
14 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';
|
|
|
|
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;
|
|
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: () {}, // TODO: Show participants
|
|
),
|
|
],
|
|
),
|
|
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;
|
|
}
|
|
}
|