diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart new file mode 100644 index 00000000..431720af --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +part 'events_event.dart'; +part 'events_state.dart'; + +class CalendarEventsBloc extends Bloc { + final EventController eventController = EventController(); + + CalendarEventsBloc() : super(EventsInitial()) { + on(_onLoadEvents); + on(_onAddEvent); + on(_onStartTimer); + on(_onDisposeResources); + on(_onGoToWeek); + } + + Future _onLoadEvents( + LoadEvents event, + Emitter emit, + ) async { + emit(EventsLoading()); + try { + final events = _generateDummyEventsForWeek(event.weekStart); + eventController.addAll(events); + emit(EventsLoaded( + events: events, + initialDate: event.weekStart, + weekDays: _getWeekDays(event.weekStart), + )); + } catch (e) { + emit(EventsError('Failed to load events')); + } + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final events = []; + + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + + return events; + } + + void _onAddEvent(AddEvent event, Emitter emit) { + eventController.add(event.event); + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + emit(EventsLoaded( + events: [...eventController.events], + initialDate: loaded.initialDate, + weekDays: loaded.weekDays, + )); + } + } + + void _onStartTimer(StartTimer event, Emitter emit) {} + + void _onDisposeResources( + DisposeResources event, Emitter emit) { + eventController.dispose(); + } + + void _onGoToWeek(GoToWeek event, Emitter emit) { + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + final newWeekDays = _getWeekDays(event.weekDate); + emit(EventsLoaded( + events: loaded.events, + initialDate: event.weekDate, + weekDays: newWeekDays, + )); + } + } + + List _generateDummyEvents() { + final now = DateTime.now(); + return [ + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 8, minute: 00, second: 0), + endTime: now.copyWith(hour: 9, minute: 00, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 9, minute: 00, second: 0), + endTime: now.copyWith(hour: 10, minute: 30, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now.add(const Duration(days: 1)), + startTime: now.copyWith(hour: 14, day: now.day + 1), + endTime: now.copyWith(hour: 15, day: now.day + 1), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + ), + CalendarEventData( + date: now.add(const Duration(days: 2)), + startTime: now.copyWith(hour: 11, day: now.day + 2), + endTime: now.copyWith(hour: 12, day: now.day + 2), + title: 'Lunch with Team', + color: Colors.orange, + ), + ]; + } + + List _getWeekDays(DateTime date) { + final int weekday = date.weekday; + final DateTime monday = date.subtract(Duration(days: weekday - 1)); + return List.generate(7, (i) => monday.add(Duration(days: i))); + } + + @override + Future close() { + eventController.dispose(); + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart new file mode 100644 index 00000000..e23e65de --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventsEvent { + const CalendarEventsEvent(); +} + +class LoadEvents extends CalendarEventsEvent { + final DateTime weekStart; + const LoadEvents({required this.weekStart}); +} + +class AddEvent extends CalendarEventsEvent { + final CalendarEventData event; + AddEvent(this.event); +} + +class StartTimer extends CalendarEventsEvent {} + +class DisposeResources extends CalendarEventsEvent {} + +class GoToWeek extends CalendarEventsEvent { + final DateTime weekDate; + GoToWeek(this.weekDate); +} diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart new file mode 100644 index 00000000..b7263ec8 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventState {} + +class EventsInitial extends CalendarEventState {} + +class EventsLoading extends CalendarEventState {} + +class EventsLoaded extends CalendarEventState { + final List events; + final DateTime initialDate; + final List weekDays; + + EventsLoaded({ + required this.events, + required this.initialDate, + required this.weekDays, + }); +} + +class EventsError extends CalendarEventState { + final String message; + EventsError(this.message); +} diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart new file mode 100644 index 00000000..6cf56fc7 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart @@ -0,0 +1,37 @@ +import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'date_selection_state.dart'; + +class DateSelectionBloc extends Bloc { + DateSelectionBloc() : super(DateSelectionState.initial()) { + on((event, emit) { + final newWeekStart = _getStartOfWeek(event.selectedDate); + emit(DateSelectionState( + selectedDate: event.selectedDate, + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + final newWeekStart = state.weekStart.add(const Duration(days: 7)); + final inNewWeek = state.selectedDate + .isAfter(newWeekStart.subtract(const Duration(days: 1))) && + state.selectedDate + .isBefore(newWeekStart.add(const Duration(days: 7))); + emit(DateSelectionState( + selectedDate: state.selectedDate, + weekStart: newWeekStart, + )); + }); + on((event, emit) { + emit(DateSelectionState( + selectedDate: state.selectedDate!.subtract(const Duration(days: 7)), + weekStart: state.weekStart.subtract(const Duration(days: 7)), + )); + }); + } + + static DateTime _getStartOfWeek(DateTime date) { + return date.subtract(Duration(days: date.weekday - 1)); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart new file mode 100644 index 00000000..8ed0a8a0 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart @@ -0,0 +1,13 @@ + +abstract class DateSelectionEvent { + const DateSelectionEvent(); +} + +class SelectDate extends DateSelectionEvent { + final DateTime selectedDate; + const SelectDate(this.selectedDate); +} + +class NextWeek extends DateSelectionEvent {} + +class PreviousWeek extends DateSelectionEvent {} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart new file mode 100644 index 00000000..3b35ce25 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart @@ -0,0 +1,21 @@ +class DateSelectionState { + final DateTime selectedDate; + final DateTime weekStart; + + const DateSelectionState({ + required this.selectedDate, + required this.weekStart, + }); + + factory DateSelectionState.initial() { + final now = DateTime.now(); + return DateSelectionState( + selectedDate: now, + weekStart: _getStartOfWeek(now), + ); + } + + static DateTime _getStartOfWeek(DateTime date) { + return date.subtract(Duration(days: date.weekday - 1)); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart new file mode 100644 index 00000000..23eaff61 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart @@ -0,0 +1,12 @@ +import 'package:bloc/bloc.dart'; +part 'selected_bookable_space_event.dart'; +part 'selected_bookable_space_state.dart'; + +class SelectedBookableSpaceBloc + extends Bloc { + SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) { + on((event, emit) { + emit(SelectedBookableSpaceState(selectedSpaceId: event.spaceId)); + }); + } +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart new file mode 100644 index 00000000..d7fc931c --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart @@ -0,0 +1,11 @@ +part of 'selected_bookable_space_bloc.dart'; + +abstract class SelectedBookableSpaceEvent { + const SelectedBookableSpaceEvent(); +} + +class SelectBookableSpace extends SelectedBookableSpaceEvent { + final dynamic spaceId; + + const SelectBookableSpace(this.spaceId); +} diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart new file mode 100644 index 00000000..98d65fde --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart @@ -0,0 +1,7 @@ +part of 'selected_bookable_space_bloc.dart'; + +class SelectedBookableSpaceState { + final String? selectedSpaceId; + + const SelectedBookableSpaceState({this.selectedSpaceId}); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart new file mode 100644 index 00000000..9d1b4a5b --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart @@ -0,0 +1,68 @@ + + + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SidebarBloc extends Bloc { + SidebarBloc() : super(SidebarState( + allRooms: [], + displayedRooms: [], + isLoading: true, + )) { + on(_onLoadRooms); + on(_onSelectRoom); + on(_onSearchRooms); + } + + Future _onLoadRooms( + LoadRoomsEvent event, + Emitter emit, + ) async { + try { + emit(state.copyWith(isLoading: true)); + + await Future.delayed(const Duration(seconds: 1)); + final rooms = List.generate(15, (index) => BookableRoom( + id: index, + name: 'Meeting Room ${index + 1}', + capacity: [4, 6, 8, 10][index % 4], + iconAsset: Assets.AtoZIcon, + )); + + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load rooms', + )); + } + } + + void _onSelectRoom( + SelectRoomEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedRoomId: event.roomId)); + } + + void _onSearchRooms( + SearchRoomsEvent event, + Emitter emit, + ) { + if (event.query.isEmpty) { + emit(state.copyWith(displayedRooms: state.allRooms)); + } else { + final filtered = state.allRooms.where((room) => + room.name.toLowerCase().contains(event.query.toLowerCase())).toList(); + emit(state.copyWith(displayedRooms: filtered)); + } + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart new file mode 100644 index 00000000..3fa504ef --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart @@ -0,0 +1,16 @@ + +abstract class SidebarEvent {} + +class LoadRoomsEvent extends SidebarEvent {} + +class SelectRoomEvent extends SidebarEvent { + final int roomId; + + SelectRoomEvent(this.roomId); +} + +class SearchRoomsEvent extends SidebarEvent { + final String query; + + SearchRoomsEvent(this.query); +} diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart new file mode 100644 index 00000000..5b30a9a0 --- /dev/null +++ b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart @@ -0,0 +1,37 @@ + + + + +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; + +class SidebarState { + final List allRooms; + final List displayedRooms; + final int? selectedRoomId; + final bool isLoading; + final String? errorMessage; + + SidebarState({ + required this.allRooms, + required this.displayedRooms, + this.selectedRoomId, + this.isLoading = false, + this.errorMessage, + }); + + SidebarState copyWith({ + List? allRooms, + List? displayedRooms, + int? selectedRoomId, + bool? isLoading, + String? errorMessage, + }) { + return SidebarState( + allRooms: allRooms ?? this.allRooms, + displayedRooms: displayedRooms ?? this.displayedRooms, + selectedRoomId: selectedRoomId ?? this.selectedRoomId, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + ); + } +} diff --git a/lib/pages/access_management/booking_system/model/bookable_room.dart b/lib/pages/access_management/booking_system/model/bookable_room.dart new file mode 100644 index 00000000..9f85984e --- /dev/null +++ b/lib/pages/access_management/booking_system/model/bookable_room.dart @@ -0,0 +1,13 @@ +class BookableRoom { + final int id; + final String name; + final int capacity; + final String? iconAsset; + + BookableRoom({ + required this.id, + required this.name, + this.capacity = 4, + this.iconAsset, + }); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/view/booking_page.dart index 6fdb53bd..072dfcd7 100644 --- a/lib/pages/access_management/booking_system/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/view/booking_page.dart @@ -1,52 +1,235 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/booking_sidebar.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class BookingPage extends StatelessWidget { +class BookingPage extends StatefulWidget { const BookingPage({super.key}); + @override + State createState() => _BookingPageState(); +} + +class _BookingPageState extends State { + late final EventController _eventController; + + @override + void initState() { + super.initState(); + _eventController = EventController(); + } + + @override + void dispose() { + _eventController.dispose(); + super.dispose(); + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final List events = []; + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + return events; + } + + void _loadEventsForWeek(DateTime weekStart) { + _eventController.removeWhere((_) => true); + _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + } + @override Widget build(BuildContext context) { - return Container( - child: Row( - children: [ - Expanded( - child: Container( - color: Colors.blueGrey[100], - child: const Center( - child: Text( - 'Side bar', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => SelectedBookableSpaceBloc()), + BlocProvider(create: (_) => DateSelectionBloc()), + ], + child: BlocListener( + listenWhen: (previous, current) => + previous.weekStart != current.weekStart, + listener: (context, state) { + _loadEventsForWeek(state.weekStart); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (id) { + context + .read() + .add(SelectBookableSpace(id)); + }, + ); + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return Container( + color: Colors.grey[300], + child: CustomCalendarPage( + selectedDate: dateState.selectedDate, + onDateChanged: (day, month, year) { + final newDate = DateTime(year, month, day); + context + .read() + .add(SelectDate(newDate)); + }, + ), + ); + }, + ), + ), + ], ), ), - )), - Expanded( + Expanded( flex: 4, child: Padding( padding: const EdgeInsets.all(20.0), - child: SizedBox( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SvgTextButton( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgTextButton( svgAsset: Assets.homeIcon, label: 'Manage Bookable Spaces', - onPressed: () {}), - SizedBox(width: 20), - SvgTextButton( + onPressed: () {}, + ), + const SizedBox(width: 20), + SvgTextButton( svgAsset: Assets.groupIcon, label: 'Manage Users', - onPressed: () {}) - ], - ) - ], - ), + onPressed: () {}, + ), + ], + ), + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.textGray, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios, + color: Colors.black), + onPressed: () { + context + .read() + .add(PreviousWeek()); + }, + ), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios, + color: Colors.black), + onPressed: () { + context + .read() + .add(NextWeek()); + }, + ), + ], + ), + ); + }, + ), + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return WeeklyCalendarPage( + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + ); + }, + ), + ), + ], ), - )) - ], + ), + ), + ], + ), ), ); } + + String _getMonthYearText(DateTime start, DateTime end) { + final startMonth = DateFormat('MMM').format(start); + final endMonth = DateFormat('MMM').format(end); + final year = start.year == end.year + ? start.year.toString() + : '${start.year}-${end.year}'; + + if (start.month == end.month) { + return '$startMonth $year'; + } else { + return '$startMonth - $endMonth $year'; + } + } } diff --git a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart new file mode 100644 index 00000000..2849da5d --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/room_list_item.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingSidebar extends StatelessWidget { + final void Function(int) onRoomSelected; + + const BookingSidebar({ + super.key, + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SidebarBloc()..add(LoadRoomsEvent()), + child: _SidebarContent(onRoomSelected: onRoomSelected), + ); + } +} + +class _SidebarContent extends StatelessWidget { + final void Function(int) onRoomSelected; + + const _SidebarContent({ + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + final TextEditingController searchController = TextEditingController(); + + return Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, + ), + ], + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + const _SidebarHeader(title: 'Spaces'), + Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, -2), + blurRadius: 4, + spreadRadius: 0, + ), + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: TextField( + style: + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + ), + controller: searchController, + onChanged: (value) { + context + .read() + .add(SearchRoomsEvent(value)); + }, + decoration: InputDecoration( + hintText: 'Search', + suffixIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + height: 20, + child: SvgPicture.asset( + Assets.searchIconUser, + color: ColorsManager.primaryTextColor, + ), + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none), + ), + ), + ), + ), + ), + ), + ), + if (state.isLoading) + const Expanded( + child: Center(child: CircularProgressIndicator()), + ) + else if (state.errorMessage != null) + Expanded( + child: Center(child: Text(state.errorMessage!)), + ) + else + Expanded( + child: ListView.builder( + itemCount: state.displayedRooms.length, + itemBuilder: (context, index) { + final room = state.displayedRooms[index]; + return RoomListItem( + room: room, + isSelected: state.selectedRoomId == room.id, + onTap: () { + context + .read() + .add(SelectRoomEvent(room.id)); + onRoomSelected(room.id); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _SidebarHeader extends StatelessWidget { + final String title; + + const _SidebarHeader({ + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.primaryTextColor, + fontSize: 20, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart new file mode 100644 index 00000000..a523ae61 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class CustomCalendarPage extends StatefulWidget { + final DateTime selectedDate; + final Function(int day, int month, int year) onDateChanged; + + const CustomCalendarPage({ + super.key, + required this.selectedDate, + required this.onDateChanged, + }); + + @override + State createState() => _CustomCalendarPageState(); +} + +class _CustomCalendarPageState extends State { + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.selectedDate; + } + + @override + void didUpdateWidget(CustomCalendarPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedDate != oldWidget.selectedDate) { + setState(() { + _selectedDate = widget.selectedDate; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + Expanded( + child: CalendarDatePicker( + initialDate: _selectedDate, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + onDateChanged: (date) { + widget.onDateChanged(date.day, date.month, date.year); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart new file mode 100644 index 00000000..40f148ff --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart @@ -0,0 +1,51 @@ + +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoomListItem extends StatelessWidget { + final BookableRoom room; + final bool isSelected; + final VoidCallback onTap; + + const RoomListItem({ + required this.room, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + hoverColor: ColorsManager.primaryColor.withOpacity(0.05), + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + IgnorePointer( + child: Radio( + value: room.id, + groupValue: isSelected ? room.id : null, + onChanged: (value) {}, + activeColor: ColorsManager.primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + room.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart new file mode 100644 index 00000000..c6eb2f40 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeeklyCalendarPage extends StatelessWidget { + final DateTime weekStart; + final DateTime selectedDate; + final EventController eventController; + + const WeeklyCalendarPage({ + super.key, + required this.weekStart, + required this.selectedDate, + required this.eventController, + }); + + @override + Widget build(BuildContext context) { + final weekDays = _getWeekDays(weekStart); + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + + return LayoutBuilder( + builder: (context, constraints) { + final double calendarWidth = constraints.maxWidth; + const double timeLineWidth = 80; + const int totalDays = 7; + final double dayColumnWidth = + (calendarWidth - timeLineWidth) / totalDays; + final selectedDayIndex = (selectedDate != null) + ? weekDays.indexWhere((d) => isSameDay(d, selectedDate)) + : -1; + return Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), + child: Stack( + children: [ + WeekView( + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: 7, + endHour: 18, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, + ), + weekDayBuilder: (date) { + final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = + weekDays.indexWhere((d) => isSameDay(d, selectedDate)); + final index = weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + final isToday = isSameDay(date, DateTime.now()); + + return Container( + decoration: isSelectedDay + ? BoxDecoration( + color: ColorsManager.blue1.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ) + : isToday + ? BoxDecoration( + color: ColorsManager.blue1.withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + ) + : null, + child: Column( + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, + ), + ), + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ), + ); + }, + timeLineBuilder: (date) { + int hour = date.hour == 0 + ? 12 + : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 60, + weekNumberBuilder: (firstDayOfWeek) => Text( + firstDayOfWeek.timeZoneName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + eventTileBuilder: (date, events, boundary, start, end) { + return Container( + margin: + const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = event.endTime != null && + event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.grayColor + : ColorsManager.lightGrayColor + .withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + }, + ), + // Highlight the selected day column + if (selectedDayIndex >= 0) + Positioned( + left: timeLineWidth + dayColumnWidth * selectedDayIndex, + top: 0, + bottom: 0, + width: dayColumnWidth, + child: IgnorePointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 0, horizontal: 2), + color: ColorsManager.blue1.withOpacity(0.1), + ), + ), + ), + Positioned( + right: 0, + top: 50, + bottom: 0, + child: IgnorePointer( + child: Container( + width: 1, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + ), + ], + ), + ); + }, + ); + } + + List _getWeekDays(DateTime date) { + final int weekday = date.weekday; + final DateTime monday = date.subtract(Duration(days: weekday - 1)); + return List.generate(7, (i) => monday.add(Duration(days: i))); + } +} + +bool isSameDay(DateTime d1, DateTime d2) { + return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; +} diff --git a/pubspec.yaml b/pubspec.yaml index c4692ac4..cba59019 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,9 @@ dependencies: bloc: ^9.0.0 geocoding: ^4.0.0 gauge_indicator: ^0.4.3 + # syncfusion_flutter_calendar: ^30.1.38 + calendar_view: ^1.4.0 + dev_dependencies: