diff --git a/apps/mobile/packages/features/staff/availability/all_errors.txt b/apps/mobile/packages/features/staff/availability/all_errors.txt deleted file mode 100644 index ab87b562..00000000 Binary files a/apps/mobile/packages/features/staff/availability/all_errors.txt and /dev/null differ diff --git a/apps/mobile/packages/features/staff/availability/errors.txt b/apps/mobile/packages/features/staff/availability/errors.txt deleted file mode 100644 index 0559cb8b..00000000 Binary files a/apps/mobile/packages/features/staff/availability/errors.txt and /dev/null differ diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart index 97c43cd4..cf6a39c1 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart @@ -1,8 +1,15 @@ +import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; import 'package:intl/intl.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../blocs/availability_bloc.dart'; +import '../blocs/availability_event.dart'; +import '../blocs/availability_state.dart'; +import 'package:krow_domain/krow_domain.dart'; + class AvailabilityPage extends StatefulWidget { const AvailabilityPage({super.key}); @@ -11,216 +18,73 @@ class AvailabilityPage extends StatefulWidget { } class _AvailabilityPageState extends State { - late DateTime _currentWeekStart; - late DateTime _selectedDate; - - // Mock Availability State - // Map of day name (lowercase) to availability status - Map _availability = { - 'monday': true, - 'tuesday': true, - 'wednesday': true, - 'thursday': true, - 'friday': true, - 'saturday': false, - 'sunday': false, - }; - - // Map of day name to time slot map - Map> _timeSlotAvailability = { - 'monday': {'morning': true, 'afternoon': true, 'evening': true}, - 'tuesday': {'morning': true, 'afternoon': true, 'evening': true}, - 'wednesday': {'morning': true, 'afternoon': true, 'evening': true}, - 'thursday': {'morning': true, 'afternoon': true, 'evening': true}, - 'friday': {'morning': true, 'afternoon': true, 'evening': true}, - 'saturday': {'morning': false, 'afternoon': false, 'evening': false}, - 'sunday': {'morning': false, 'afternoon': false, 'evening': false}, - }; - - final List _dayNames = [ - 'sunday', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - ]; - - final List> _timeSlots = [ - { - 'slotId': 'morning', - 'label': 'Morning', - 'timeRange': '4:00 AM - 12:00 PM', - 'icon': LucideIcons.sunrise, - 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 - 'iconColor': const Color(0xFF0032A0), - }, - { - 'slotId': 'afternoon', - 'label': 'Afternoon', - 'timeRange': '12:00 PM - 6:00 PM', - 'icon': LucideIcons.sun, - 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 - 'iconColor': const Color(0xFF0032A0), - }, - { - 'slotId': 'evening', - 'label': 'Evening', - 'timeRange': '6:00 PM - 12:00 AM', - 'icon': LucideIcons.moon, - 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 - 'iconColor': const Color(0xFF333F48), - }, - ]; + final AvailabilityBloc _bloc = Modular.get(); @override void initState() { super.initState(); + _calculateInitialWeek(); + } + + void _calculateInitialWeek() { final today = DateTime.now(); - - // Dart equivalent for Monday start: final day = today.weekday; // Mon=1, Sun=7 - final diff = day - 1; - _currentWeekStart = today.subtract(Duration(days: diff)); - // Reset time to midnight - _currentWeekStart = DateTime( - _currentWeekStart.year, - _currentWeekStart.month, - _currentWeekStart.day, + final diff = day - 1; // Assuming Monday start + DateTime currentWeekStart = today.subtract(Duration(days: diff)); + currentWeekStart = DateTime( + currentWeekStart.year, + currentWeekStart.month, + currentWeekStart.day, ); - - _selectedDate = today; - } - - List _getWeekDates() { - return List.generate( - 7, - (index) => _currentWeekStart.add(Duration(days: index)), - ); - } - - String _formatDay(DateTime date) { - return DateFormat('EEE').format(date); - } - - bool _isToday(DateTime date) { - final now = DateTime.now(); - return date.year == now.year && - date.month == now.month && - date.day == now.day; - } - - bool _isSelected(DateTime date) { - return date.year == _selectedDate.year && - date.month == _selectedDate.month && - date.day == _selectedDate.day; - } - - void _navigateWeek(int direction) { - setState(() { - _currentWeekStart = _currentWeekStart.add(Duration(days: direction * 7)); - }); - } - - void _toggleDayAvailability(String dayName) { - setState(() { - _availability[dayName] = !(_availability[dayName] ?? false); - // React code also updates mutation. We mock this. - // NOTE: In prototype we mock it. Refactor will move this to BLoC. - }); - } - - String _getDayKey(DateTime date) { - // DateTime.weekday: Mon=1...Sun=7. - // _dayNames array: 0=Sun, 1=Mon... - // Dart weekday: 7 is Sunday. 7 % 7 = 0. - return _dayNames[date.weekday % 7]; - } - - void _toggleTimeSlot(String slotId) { - final dayKey = _getDayKey(_selectedDate); - final currentDaySlots = - _timeSlotAvailability[dayKey] ?? - {'morning': true, 'afternoon': true, 'evening': true}; - final newValue = !(currentDaySlots[slotId] ?? true); - - setState(() { - _timeSlotAvailability[dayKey] = {...currentDaySlots, slotId: newValue}; - }); - } - - bool _isTimeSlotActive(String slotId) { - final dayKey = _getDayKey(_selectedDate); - final daySlots = _timeSlotAvailability[dayKey]; - if (daySlots == null) return true; - return daySlots[slotId] != false; - } - - String _getMonthYear() { - final middleDate = _currentWeekStart.add(const Duration(days: 3)); - return DateFormat('MMMM yyyy').format(middleDate); - } - - void _quickSet(String type) { - Map newAvailability = {}; - - switch (type) { - case 'all': - for (var day in _dayNames) newAvailability[day] = true; - break; - case 'weekdays': - for (var day in _dayNames) - newAvailability[day] = (day != 'saturday' && day != 'sunday'); - break; - case 'weekends': - for (var day in _dayNames) - newAvailability[day] = (day == 'saturday' || day == 'sunday'); - break; - case 'clear': - for (var day in _dayNames) newAvailability[day] = false; - break; - } - - setState(() { - _availability = newAvailability; - }); + _bloc.add(LoadAvailability(currentWeekStart)); } @override Widget build(BuildContext context) { - final selectedDayKey = _getDayKey(_selectedDate); - final isSelectedDayAvailable = _availability[selectedDayKey] ?? false; - final weekDates = _getWeekDates(); - - return Scaffold( - backgroundColor: const Color( - 0xFFFAFBFC, - ), // slate-50 to white gradient approximation - body: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( - children: [ - _buildHeader(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildQuickSet(), - const SizedBox(height: 24), - _buildWeekNavigation(weekDates), - const SizedBox(height: 24), - _buildSelectedDayAvailability( - selectedDayKey, - isSelectedDayAvailable, - ), - const SizedBox(height: 24), - _buildInfoCard(), - ], - ), - ), - ], + return BlocProvider.value( + value: _bloc, + child: Scaffold( + backgroundColor: AppColors.krowBackground, + appBar: UiAppBar( + title: 'My Availability', + showBackButton: true, + ), + body: BlocBuilder( + builder: (context, state) { + if (state is AvailabilityLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is AvailabilityLoaded) { + return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), + child: Column( + children: [ + //_buildHeader(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildQuickSet(context), + const SizedBox(height: 24), + _buildWeekNavigation(context, state), + const SizedBox(height: 24), + _buildSelectedDayAvailability( + context, + state.selectedDayAvailability, + ), + const SizedBox(height: 24), + _buildInfoCard(), + ], + ), + ), + ], + ), + ); + } else if (state is AvailabilityError) { + return Center(child: Text('Error: ${state.message}')); + } + return const SizedBox.shrink(); + }, ), ), ); @@ -244,73 +108,28 @@ class _AvailabilityPageState extends State { onPressed: () => Modular.to.pop(), ), const SizedBox(width: 12), - Row( + const Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.krowBlue.withOpacity(0.2), - width: 2, - ), - shape: BoxShape.circle, - ), - child: Center( - child: CircleAvatar( - backgroundColor: AppColors.krowBlue.withOpacity( - 0.1, - ), - radius: 18, - child: const Text( - 'K', // Mock initial - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), + Text( + 'My Availability', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.krowCharcoal, ), ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'My Availability', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - 'Set when you can work', - style: TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], + Text( + 'Set when you can work', + style: TextStyle( + fontSize: 14, + color: AppColors.krowMuted, + ), ), ], ), ], ), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.calendar, - color: AppColors.krowBlue, - size: 20, - ), - ), ], ), ], @@ -318,7 +137,7 @@ class _AvailabilityPageState extends State { ); } - Widget _buildQuickSet() { + Widget _buildQuickSet(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -340,27 +159,34 @@ class _AvailabilityPageState extends State { Row( children: [ Expanded( - child: _buildQuickSetButton('All Week', () => _quickSet('all')), + child: _buildQuickSetButton( + context, + 'All Week', + 'all', + ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Weekdays', - () => _quickSet('weekdays'), + 'weekdays', ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Weekends', - () => _quickSet('weekends'), + 'weekends', ), ), const SizedBox(width: 8), Expanded( child: _buildQuickSetButton( + context, 'Clear All', - () => _quickSet('clear'), + 'clear', isDestructive: true, ), ), @@ -372,14 +198,15 @@ class _AvailabilityPageState extends State { } Widget _buildQuickSetButton( + BuildContext context, String label, - VoidCallback onTap, { + String type, { bool isDestructive = false, }) { return SizedBox( height: 32, child: OutlinedButton( - onPressed: onTap, + onPressed: () => context.read().add(PerformQuickSet(type)), style: OutlinedButton.styleFrom( padding: EdgeInsets.zero, side: BorderSide( @@ -387,8 +214,7 @@ class _AvailabilityPageState extends State { ? Colors.red.withOpacity(0.2) : AppColors.krowBlue.withOpacity(0.2), ), - backgroundColor: - Colors.transparent, + backgroundColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -404,7 +230,11 @@ class _AvailabilityPageState extends State { ); } - Widget _buildWeekNavigation(List weekDates) { + Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) { + // Middle date for month display + final middleDate = state.currentWeekStart.add(const Duration(days: 3)); + final monthYear = DateFormat('MMMM yyyy').format(middleDate); + return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -429,10 +259,10 @@ class _AvailabilityPageState extends State { children: [ _buildNavButton( LucideIcons.chevronLeft, - () => _navigateWeek(-1), + () => context.read().add(const NavigateWeek(-1)), ), Text( - _getMonthYear(), + monthYear, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -441,7 +271,7 @@ class _AvailabilityPageState extends State { ), _buildNavButton( LucideIcons.chevronRight, - () => _navigateWeek(1), + () => context.read().add(const NavigateWeek(1)), ), ], ), @@ -449,7 +279,7 @@ class _AvailabilityPageState extends State { // Days Row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: weekDates.map((date) => _buildDayItem(date)).toList(), + children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(), ), ], ), @@ -471,15 +301,14 @@ class _AvailabilityPageState extends State { ); } - Widget _buildDayItem(DateTime date) { - final isSelected = _isSelected(date); - final dayKey = _getDayKey(date); - final isAvailable = _availability[dayKey] ?? false; - final isToday = _isToday(date); + Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) { + final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate); + final isAvailable = day.isAvailable; + final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now()); return Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedDate = date), + onTap: () => context.read().add(SelectDate(day.date)), child: Container( margin: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(vertical: 12), @@ -514,7 +343,7 @@ class _AvailabilityPageState extends State { Column( children: [ Text( - date.day.toString().padLeft(2, '0'), + day.date.day.toString().padLeft(2, '0'), style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -527,7 +356,7 @@ class _AvailabilityPageState extends State { ), const SizedBox(height: 2), Text( - _formatDay(date), + DateFormat('EEE').format(day.date), style: TextStyle( fontSize: 10, color: isSelected @@ -559,10 +388,11 @@ class _AvailabilityPageState extends State { } Widget _buildSelectedDayAvailability( - String selectedDayKey, - bool isAvailable, + BuildContext context, + DayAvailability day, ) { - final dateStr = DateFormat('EEEE, MMM d').format(_selectedDate); + final dateStr = DateFormat('EEEE, MMM d').format(day.date); + final isAvailable = day.isAvailable; return Container( padding: const EdgeInsets.all(20), @@ -606,7 +436,7 @@ class _AvailabilityPageState extends State { ), Switch( value: isAvailable, - onChanged: (val) => _toggleDayAvailability(selectedDayKey), + onChanged: (val) => context.read().add(ToggleDayStatus(day)), activeColor: AppColors.krowBlue, ), ], @@ -614,123 +444,163 @@ class _AvailabilityPageState extends State { const SizedBox(height: 16), - // Time Slots - ..._timeSlots.map((slot) { - final isActive = _isTimeSlotActive(slot['slotId']); - // Determine styles based on state - final isEnabled = - isAvailable; // If day is off, slots are disabled visually - - // Container style - Color bgColor; - Color borderColor; - - if (!isEnabled) { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFF1F5F9); // slate-100 - } else if (isActive) { - bgColor = AppColors.krowBlue.withOpacity(0.05); - borderColor = AppColors.krowBlue.withOpacity(0.2); - } else { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFE2E8F0); // slate-200 - } - - // Text colors - final titleColor = (isEnabled && isActive) - ? AppColors.krowCharcoal - : AppColors.krowMuted; - final subtitleColor = (isEnabled && isActive) - ? AppColors.krowMuted - : Colors.grey.shade400; - - return GestureDetector( - onTap: isEnabled ? () => _toggleTimeSlot(slot['slotId']) : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: borderColor, width: 2), - ), - child: Row( - children: [ - // Icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: slot['bg'], - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - slot['icon'], - color: slot['iconColor'], - size: 20, - ), - ), - const SizedBox(width: 12), - // Text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - slot['label'], - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: titleColor, - ), - ), - Text( - slot['timeRange'], - style: TextStyle( - fontSize: 12, - color: subtitleColor, - ), - ), - ], - ), - ), - // Checkbox indicator - if (isEnabled && isActive) - Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - size: 16, - color: Colors.white, - ), - ) - else if (isEnabled && !isActive) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFFCBD5E1), - width: 2, - ), // slate-300 - ), - ), - ], - ), - ), - ); + // Time Slots (only from Domain) + ...day.slots.map((slot) { + // Get UI config for this slot ID + final uiConfig = _getSlotUiConfig(slot.id); + + return _buildTimeSlotItem(context, day, slot, uiConfig); }).toList(), ], ), ); } + + Map _getSlotUiConfig(String slotId) { + switch (slotId) { + case 'morning': + return { + 'icon': LucideIcons.sunrise, + 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 + 'iconColor': const Color(0xFF0032A0), + }; + case 'afternoon': + return { + 'icon': LucideIcons.sun, + 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 + 'iconColor': const Color(0xFF0032A0), + }; + case 'evening': + return { + 'icon': LucideIcons.moon, + 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 + 'iconColor': const Color(0xFF333F48), + }; + default: + return { + 'icon': LucideIcons.clock, + 'bg': Colors.grey.shade100, + 'iconColor': Colors.grey, + }; + } + } + + Widget _buildTimeSlotItem( + BuildContext context, + DayAvailability day, + AvailabilitySlot slot, + Map uiConfig + ) { + // Determine styles based on state + final isEnabled = day.isAvailable; + final isActive = slot.isAvailable; + + // Container style + Color bgColor; + Color borderColor; + + if (!isEnabled) { + bgColor = const Color(0xFFF8FAFC); // slate-50 + borderColor = const Color(0xFFF1F5F9); // slate-100 + } else if (isActive) { + bgColor = AppColors.krowBlue.withOpacity(0.05); + borderColor = AppColors.krowBlue.withOpacity(0.2); + } else { + bgColor = const Color(0xFFF8FAFC); // slate-50 + borderColor = const Color(0xFFE2E8F0); // slate-200 + } + + // Text colors + final titleColor = (isEnabled && isActive) + ? AppColors.krowCharcoal + : AppColors.krowMuted; + final subtitleColor = (isEnabled && isActive) + ? AppColors.krowMuted + : Colors.grey.shade400; + + return GestureDetector( + onTap: isEnabled ? () => context.read().add(ToggleSlotStatus(day, slot.id)) : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor, width: 2), + ), + child: Row( + children: [ + // Icon + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: uiConfig['bg'], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + uiConfig['icon'], + color: uiConfig['iconColor'], + size: 20, + ), + ), + const SizedBox(width: 12), + // Text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + slot.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: titleColor, + ), + ), + Text( + slot.timeRange, + style: TextStyle( + fontSize: 12, + color: subtitleColor, + ), + ), + ], + ), + ), + // Checkbox indicator + if (isEnabled && isActive) + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: AppColors.krowBlue, + shape: BoxShape.circle, + ), + child: const Icon( + LucideIcons.check, + size: 16, + color: Colors.white, + ), + ) + else if (isEnabled && !isActive) + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFCBD5E1), + width: 2, + ), // slate-300 + ), + ), + ], + ), + ), + ); + } Widget _buildInfoCard() { return Container( diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart deleted file mode 100644 index ef684b8b..00000000 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page_new.dart +++ /dev/null @@ -1,693 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; -import 'package:intl/intl.dart'; -import 'package:lucide_icons/lucide_icons.dart'; - -import '../blocs/availability_bloc.dart'; -import '../blocs/availability_event.dart'; -import '../blocs/availability_state.dart'; -import 'package:krow_domain/krow_domain.dart'; - -class AvailabilityPage extends StatefulWidget { - const AvailabilityPage({super.key}); - - @override - State createState() => _AvailabilityPageState(); -} - -class _AvailabilityPageState extends State { - final AvailabilityBloc _bloc = Modular.get(); - - @override - void initState() { - super.initState(); - _calculateInitialWeek(); - } - - void _calculateInitialWeek() { - final today = DateTime.now(); - final day = today.weekday; // Mon=1, Sun=7 - final diff = day - 1; // Assuming Monday start - DateTime currentWeekStart = today.subtract(Duration(days: diff)); - currentWeekStart = DateTime( - currentWeekStart.year, - currentWeekStart.month, - currentWeekStart.day, - ); - _bloc.add(LoadAvailability(currentWeekStart)); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: Scaffold( - backgroundColor: AppColors.krowBackground, - body: BlocBuilder( - builder: (context, state) { - if (state is AvailabilityLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is AvailabilityLoaded) { - return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( - children: [ - _buildHeader(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildQuickSet(context), - const SizedBox(height: 24), - _buildWeekNavigation(context, state), - const SizedBox(height: 24), - _buildSelectedDayAvailability( - context, - state.selectedDayAvailability, - ), - const SizedBox(height: 24), - _buildInfoCard(), - ], - ), - ), - ], - ), - ); - } else if (state is AvailabilityError) { - return Center(child: Text('Error: ${state.message}')); - } - return const SizedBox.shrink(); - }, - ), - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - LucideIcons.arrowLeft, - color: AppColors.krowCharcoal, - ), - onPressed: () => Modular.to.pop(), - ), - const SizedBox(width: 12), - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.krowBlue.withOpacity(0.2), - width: 2, - ), - shape: BoxShape.circle, - ), - child: Center( - child: CircleAvatar( - backgroundColor: AppColors.krowBlue.withOpacity( - 0.1, - ), - radius: 18, - child: const Text( - 'K', // Mock initial - style: TextStyle( - color: AppColors.krowBlue, - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - ), - ), - ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'My Availability', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - 'Set when you can work', - style: TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - ], - ), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.calendar, - color: AppColors.krowBlue, - size: 20, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildQuickSet(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Quick Set Availability', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF333F48), - ), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildQuickSetButton( - context, - 'All Week', - 'all', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Weekdays', - 'weekdays', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Weekends', - 'weekends', - ), - ), - const SizedBox(width: 8), - Expanded( - child: _buildQuickSetButton( - context, - 'Clear All', - 'clear', - isDestructive: true, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildQuickSetButton( - BuildContext context, - String label, - String type, { - bool isDestructive = false, - }) { - return SizedBox( - height: 32, - child: OutlinedButton( - onPressed: () => context.read().add(PerformQuickSet(type)), - style: OutlinedButton.styleFrom( - padding: EdgeInsets.zero, - side: BorderSide( - color: isDestructive - ? Colors.red.withOpacity(0.2) - : AppColors.krowBlue.withOpacity(0.2), - ), - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - foregroundColor: isDestructive ? Colors.red : AppColors.krowBlue, - ), - child: Text( - label, - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) { - // Middle date for month display - final middleDate = state.currentWeekStart.add(const Duration(days: 3)); - final monthYear = DateFormat('MMMM yyyy').format(middleDate); - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade100), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - children: [ - // Nav Header - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildNavButton( - LucideIcons.chevronLeft, - () => context.read().add(const NavigateWeek(-1)), - ), - Text( - monthYear, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - _buildNavButton( - LucideIcons.chevronRight, - () => context.read().add(const NavigateWeek(1)), - ), - ], - ), - ), - // Days Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(), - ), - ], - ), - ); - } - - Widget _buildNavButton(IconData icon, VoidCallback onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 32, - height: 32, - decoration: const BoxDecoration( - color: Color(0xFFF1F5F9), // slate-100 - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: AppColors.krowMuted), - ), - ); - } - - Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) { - final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate); - final isAvailable = day.isAvailable; - final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now()); - - return Expanded( - child: GestureDetector( - onTap: () => context.read().add(SelectDate(day.date)), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 2), - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.krowBlue - : (isAvailable - ? const Color(0xFFECFDF5) - : const Color(0xFFF8FAFC)), // emerald-50 or slate-50 - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? AppColors.krowBlue - : (isAvailable - ? const Color(0xFFA7F3D0) - : Colors.transparent), // emerald-200 - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColors.krowBlue.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Column( - children: [ - Text( - day.date.day.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isSelected - ? Colors.white - : (isAvailable - ? const Color(0xFF047857) - : AppColors.krowMuted), // emerald-700 - ), - ), - const SizedBox(height: 2), - Text( - DateFormat('EEE').format(day.date), - style: TextStyle( - fontSize: 10, - color: isSelected - ? Colors.white.withOpacity(0.8) - : (isAvailable - ? const Color(0xFF047857) - : AppColors.krowMuted), - ), - ), - ], - ), - if (isToday && !isSelected) - Positioned( - bottom: -8, - child: Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildSelectedDayAvailability( - BuildContext context, - DayAvailability day, - ) { - final dateStr = DateFormat('EEEE, MMM d').format(day.date); - final isAvailable = day.isAvailable; - - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade100), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - children: [ - // Header Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - dateStr, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - Text( - isAvailable ? 'You are available' : 'Not available', - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - Switch( - value: isAvailable, - onChanged: (val) => context.read().add(ToggleDayStatus(day)), - activeColor: AppColors.krowBlue, - ), - ], - ), - - const SizedBox(height: 16), - - // Time Slots (only from Domain) - ...day.slots.map((slot) { - // Get UI config for this slot ID - final uiConfig = _getSlotUiConfig(slot.id); - - return _buildTimeSlotItem(context, day, slot, uiConfig); - }).toList(), - ], - ), - ); - } - - Map _getSlotUiConfig(String slotId) { - switch (slotId) { - case 'morning': - return { - 'icon': LucideIcons.sunrise, - 'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10 - 'iconColor': const Color(0xFF0032A0), - }; - case 'afternoon': - return { - 'icon': LucideIcons.sun, - 'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20 - 'iconColor': const Color(0xFF0032A0), - }; - case 'evening': - return { - 'icon': LucideIcons.moon, - 'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10 - 'iconColor': const Color(0xFF333F48), - }; - default: - return { - 'icon': LucideIcons.clock, - 'bg': Colors.grey.shade100, - 'iconColor': Colors.grey, - }; - } - } - - Widget _buildTimeSlotItem( - BuildContext context, - DayAvailability day, - AvailabilitySlot slot, - Map uiConfig - ) { - // Determine styles based on state - final isEnabled = day.isAvailable; - final isActive = slot.isAvailable; - - // Container style - Color bgColor; - Color borderColor; - - if (!isEnabled) { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFF1F5F9); // slate-100 - } else if (isActive) { - bgColor = AppColors.krowBlue.withOpacity(0.05); - borderColor = AppColors.krowBlue.withOpacity(0.2); - } else { - bgColor = const Color(0xFFF8FAFC); // slate-50 - borderColor = const Color(0xFFE2E8F0); // slate-200 - } - - // Text colors - final titleColor = (isEnabled && isActive) - ? AppColors.krowCharcoal - : AppColors.krowMuted; - final subtitleColor = (isEnabled && isActive) - ? AppColors.krowMuted - : Colors.grey.shade400; - - return GestureDetector( - onTap: isEnabled ? () => context.read().add(ToggleSlotStatus(day, slot.id)) : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: borderColor, width: 2), - ), - child: Row( - children: [ - // Icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: uiConfig['bg'], - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - uiConfig['icon'], - color: uiConfig['iconColor'], - size: 20, - ), - ), - const SizedBox(width: 12), - // Text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - slot.label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: titleColor, - ), - ), - Text( - slot.timeRange, - style: TextStyle( - fontSize: 12, - color: subtitleColor, - ), - ), - ], - ), - ), - // Checkbox indicator - if (isEnabled && isActive) - Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - child: const Icon( - LucideIcons.check, - size: 16, - color: Colors.white, - ), - ) - else if (isEnabled && !isActive) - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFFCBD5E1), - width: 2, - ), // slate-300 - ), - ), - ], - ), - ), - ); - } - - Widget _buildInfoCard() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - child: const Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(LucideIcons.clock, size: 20, color: AppColors.krowBlue), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Auto-Match uses your availability', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: AppColors.krowCharcoal, - ), - ), - SizedBox(height: 2), - Text( - "When enabled, you'll only be matched with shifts during your available times.", - style: TextStyle(fontSize: 12, color: AppColors.krowMuted), - ), - ], - ), - ), - ], - ), - ); - } -} - -class AppColors { - static const Color krowBlue = Color(0xFF0A39DF); - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = Color(0xFF121826); - static const Color krowMuted = Color(0xFF6A7382); - static const Color krowBorder = Color(0xFFE3E6E9); - static const Color krowBackground = Color(0xFFFAFBFC); - - static const Color white = Colors.white; - static const Color black = Colors.black; -} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart index 1cdda799..35aba337 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart @@ -1,7 +1,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:staff_availability/src/presentation/pages/availability_page_new.dart'; +import 'package:staff_availability/src/presentation/pages/availability_page.dart'; import 'data/repositories_impl/availability_repository_impl.dart'; import 'domain/repositories/availability_repository.dart'; diff --git a/apps/mobile/packages/features/staff/availability/pubspec.yaml b/apps/mobile/packages/features/staff/availability/pubspec.yaml index 43e38293..06f08f01 100644 --- a/apps/mobile/packages/features/staff/availability/pubspec.yaml +++ b/apps/mobile/packages/features/staff/availability/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: krow_core: path: ../../../core firebase_data_connect: ^0.2.2+2 + firebase_auth: ^6.1.4 dev_dependencies: flutter_test: