334 lines
13 KiB
Dart
334 lines
13 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.month;
|
|
|
|
@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);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Calendrier du voyage'),
|
|
backgroundColor: theme.colorScheme.surface,
|
|
foregroundColor: theme.colorScheme.onSurface,
|
|
elevation: 0,
|
|
),
|
|
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, though ideally we should be in loaded state
|
|
// This might happen if we navigate back and forth
|
|
}
|
|
|
|
// 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,
|
|
);
|
|
|
|
return Column(
|
|
children: [
|
|
TableCalendar(
|
|
firstDay: DateTime.now().subtract(const Duration(days: 365)),
|
|
lastDay: DateTime.now().add(const Duration(days: 365)),
|
|
focusedDay: _focusedDay,
|
|
calendarFormat: _calendarFormat,
|
|
selectedDayPredicate: (day) {
|
|
return isSameDay(_selectedDay, day);
|
|
},
|
|
onDaySelected: (selectedDay, focusedDay) {
|
|
setState(() {
|
|
_selectedDay = selectedDay;
|
|
_focusedDay = focusedDay;
|
|
});
|
|
},
|
|
onFormatChanged: (format) {
|
|
setState(() {
|
|
_calendarFormat = format;
|
|
});
|
|
},
|
|
onPageChanged: (focusedDay) {
|
|
_focusedDay = focusedDay;
|
|
},
|
|
eventLoader: (day) {
|
|
return _getActivitiesForDay(day, scheduledActivities);
|
|
},
|
|
calendarBuilders: CalendarBuilders(
|
|
markerBuilder: (context, day, events) {
|
|
if (events.isEmpty) return null;
|
|
return Positioned(
|
|
bottom: 1,
|
|
child: Container(
|
|
width: 7,
|
|
height: 7,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
calendarStyle: CalendarStyle(
|
|
todayDecoration: BoxDecoration(
|
|
color: theme.colorScheme.primary.withValues(alpha: 0.5),
|
|
shape: BoxShape.circle,
|
|
),
|
|
selectedDecoration: BoxDecoration(
|
|
color: theme.colorScheme.primary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
const Divider(),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
// Scheduled Activities for Selected Day
|
|
Expanded(
|
|
flex: 3,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'Activités du ${DateFormat('dd/MM/yyyy').format(_selectedDay!)}',
|
|
style: theme.textTheme.titleMedium,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: selectedActivities.isEmpty
|
|
? Center(
|
|
child: Text(
|
|
'Aucune activité prévue',
|
|
style: theme.textTheme.bodyMedium
|
|
?.copyWith(
|
|
color: theme.colorScheme.onSurface
|
|
.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: selectedActivities.length,
|
|
itemBuilder: (context, index) {
|
|
final activity =
|
|
selectedActivities[index];
|
|
return ListTile(
|
|
title: Text(activity.name),
|
|
subtitle: Text(
|
|
'${activity.category} - ${DateFormat('HH:mm').format(activity.date!)}',
|
|
),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () {
|
|
context.read<ActivityBloc>().add(
|
|
UpdateActivityDate(
|
|
tripId: widget.trip.id!,
|
|
activityId: activity.id,
|
|
date: null,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const VerticalDivider(),
|
|
// Unscheduled Activities
|
|
Expanded(
|
|
flex: 2,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'À planifier',
|
|
style: theme.textTheme.titleMedium,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: unscheduledActivities.isEmpty
|
|
? Center(
|
|
child: Text(
|
|
'Tout est planifié !',
|
|
style: theme.textTheme.bodyMedium
|
|
?.copyWith(
|
|
color: theme.colorScheme.onSurface
|
|
.withValues(alpha: 0.6),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
itemCount: unscheduledActivities.length,
|
|
itemBuilder: (context, index) {
|
|
final activity =
|
|
unscheduledActivities[index];
|
|
return Draggable<Activity>(
|
|
data: activity,
|
|
feedback: Material(
|
|
elevation: 4,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
color: theme.cardColor,
|
|
child: Text(activity.name),
|
|
),
|
|
),
|
|
child: ListTile(
|
|
title: Text(
|
|
activity.name,
|
|
style: theme.textTheme.bodySmall,
|
|
),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
if (_selectedDay != null) {
|
|
_selectTimeAndSchedule(
|
|
activity,
|
|
_selectedDay!,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// Zone de drop pour le calendrier
|
|
DragTarget<Activity>(
|
|
onWillAcceptWithDetails: (details) => true,
|
|
onAcceptWithDetails: (details) {
|
|
if (_selectedDay != null) {
|
|
_selectTimeAndSchedule(
|
|
details.data,
|
|
_selectedDay!,
|
|
);
|
|
}
|
|
},
|
|
builder: (context, candidateData, rejectedData) {
|
|
return Container(
|
|
height: 50,
|
|
color: candidateData.isNotEmpty
|
|
? theme.colorScheme.primary.withValues(
|
|
alpha: 0.1,
|
|
)
|
|
: null,
|
|
child: Center(
|
|
child: Text(
|
|
'Glisser ici pour planifier',
|
|
style: TextStyle(
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|