From 1a4a797aa34380d38da152ef6c322ac4e8b3ed3e Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 15:20:59 -0500 Subject: [PATCH 01/13] feat: integrate Google Maps Places Autocomplete for hub address validation --- .../get_started_page/get_started_actions.dart | 40 ++++--------------- .../get_started_page/get_started_header.dart | 32 +++++++++++---- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart index a4596b2d..7e7ead4b 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_actions.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; +import 'package:core_localization/core_localization.dart'; class GetStartedActions extends StatelessWidget { final VoidCallback onSignUpPressed; @@ -13,40 +14,15 @@ class GetStartedActions extends StatelessWidget { @override Widget build(BuildContext context) { + final TranslationsStaffAuthenticationGetStartedPageEn i18n = + t.staff_authentication.get_started_page; + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton( - onPressed: onSignUpPressed, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Create Account', - style: UiTypography.buttonL.copyWith(color: Colors.white), - ), - ), - const SizedBox(height: 16), - OutlinedButton( - onPressed: onLoginPressed, - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.primary, - side: const BorderSide(color: UiColors.primary), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Log In', - style: UiTypography.buttonL.copyWith(color: UiColors.primary), - ), - ), + spacing: UiConstants.space4, + children: [ + UiButton.primary(onPressed: onSignUpPressed, text: i18n.sign_up_button), + UiButton.secondary(onPressed: onLoginPressed, text: i18n.log_in_button), ], ); } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart index f6c940e1..e2b37211 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_header.dart @@ -1,26 +1,42 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; +import 'package:staff_authentication/staff_authentication.dart'; +/// A widget that displays the welcome text and description on the Get Started page. class GetStartedHeader extends StatelessWidget { + /// Creates a [GetStartedHeader]. const GetStartedHeader({super.key}); @override Widget build(BuildContext context) { + final TranslationsStaffAuthenticationGetStartedPageEn i18n = + t.staff_authentication.get_started_page; + return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Krow Workforce', - style: UiTypography.display1b.copyWith(color: UiColors.textPrimary), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RichText( textAlign: TextAlign.center, + text: TextSpan( + style: UiTypography.displayM, + children: [ + TextSpan( + text: i18n.title_part1, + ), + TextSpan( + text: i18n.title_part2, + style: UiTypography.displayMb.textLink, + ), + ], + ), ), const SizedBox(height: 16), Text( - 'Find flexible shifts that fit your schedule.', - style: UiTypography.body1r.copyWith(color: UiColors.textSecondary), + i18n.subtitle, textAlign: TextAlign.center, + style: UiTypography.body1r.textSecondary, ), ], ); } -} +} \ No newline at end of file From 3e156565c82abaf017e880a09491019461c32d81 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 17:12:10 -0500 Subject: [PATCH 02/13] feat: Add shifts styles, empty state view, and tabs for finding, history, and my shifts - Introduced AppColors for consistent color usage across the app. - Implemented EmptyStateView widget for displaying empty states with icons and messages. - Created FindShiftsTab for searching and filtering available jobs. - Developed HistoryShiftsTab to display completed shifts with an empty state. - Added MyShiftsTab for managing user shifts, including confirmation and decline functionalities. --- .../shifts_repository_impl.dart | 27 +- .../src/presentation/pages/shifts_page.dart | 682 ++++-------------- .../presentation/styles/shifts_styles.dart | 13 + .../widgets/shared/empty_state_view.dart | 69 ++ .../widgets/tabs/find_shifts_tab.dart | 201 ++++++ .../widgets/tabs/history_shifts_tab.dart | 44 ++ .../widgets/tabs/my_shifts_tab.dart | 582 +++++++++++++++ 7 files changed, 1039 insertions(+), 579 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 4ffc3563..584a4bcd 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -51,28 +51,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { DateTime? _toDateTime(dynamic t) { if (t == null) return null; - if (t is DateTime) return t; - if (t is String) return DateTime.tryParse(t); - - // Data Connect Timestamp handling try { - if (t is Timestamp) { - return t.toDateTime(); + return DateTime.tryParse(t.toJson() as String); + } catch (_) { + try { + return DateTime.tryParse(t.toString()); + } catch (e) { + return null; } - } catch (_) {} - - try { - // Fallback for any object that might have a toDate or similar - if (t.runtimeType.toString().contains('Timestamp')) { - return (t as dynamic).toDate(); - } - } catch (_) {} - - try { - return DateTime.tryParse(t.toString()); - } catch (_) {} - - return null; + } } @override diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index adf0e07e..1a0dd2a5 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -1,25 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:lucide_icons/lucide_icons.dart'; -import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; import '../blocs/shifts/shifts_bloc.dart'; -import '../widgets/my_shift_card.dart'; -import '../widgets/shift_assignment_card.dart'; - -// Shim to match POC styles locally -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = UiColors.textPrimary; - static const Color krowMuted = UiColors.textSecondary; - static const Color krowBorder = UiColors.border; - static const Color krowBackground = UiColors.background; - static const Color white = Colors.white; - static const Color black = Colors.black; -} +import '../widgets/tabs/my_shifts_tab.dart'; +import '../widgets/tabs/find_shifts_tab.dart'; +import '../widgets/tabs/history_shifts_tab.dart'; +import '../styles/shifts_styles.dart'; class ShiftsPage extends StatefulWidget { final String? initialTab; @@ -31,15 +19,6 @@ class ShiftsPage extends StatefulWidget { class _ShiftsPageState extends State { late String _activeTab; - String _searchQuery = ''; - // ignore: unused_field - String? _cancelledShiftDemo; // 'lastMinute' or 'advance' - String _jobType = 'all'; // all, one-day, multi-day, long-term - - // Calendar State - DateTime _selectedDate = DateTime.now(); - int _weekOffset = 0; - final ShiftsBloc _bloc = Modular.get(); @override @@ -59,93 +38,30 @@ class _ShiftsPageState extends State { } } - List _getCalendarDays() { - final now = DateTime.now(); - int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; - int daysSinceFriday = (reactDayIndex + 2) % 7; - final start = now - .subtract(Duration(days: daysSinceFriday)) - .add(Duration(days: _weekOffset * 7)); - final startDate = DateTime(start.year, start.month, start.day); - return List.generate(7, (index) => startDate.add(Duration(days: index))); - } - - bool _isSameDay(DateTime a, DateTime b) { - return a.year == b.year && a.month == b.month && a.day == b.day; - } - - void _confirmShift(String id) { - _bloc.add(AcceptShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift confirmed!'), - backgroundColor: Color(0xFF10B981), - ), - ); - } - - void _declineShift(String id) { - _bloc.add(DeclineShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift declined.'), - backgroundColor: Color(0xFFEF4444), - ), - ); - } - @override Widget build(BuildContext context) { return BlocProvider.value( value: _bloc, child: BlocBuilder( builder: (context, state) { - final List myShifts = (state is ShiftsLoaded) ? state.myShifts : []; - final List availableJobs = (state is ShiftsLoaded) ? state.availableShifts : []; - final List pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : []; - final List cancelledShifts = (state is ShiftsLoaded) ? state.cancelledShifts : []; - final List historyShifts = (state is ShiftsLoaded) ? state.historyShifts : []; + final List myShifts = (state is ShiftsLoaded) + ? state.myShifts + : []; + final List availableJobs = (state is ShiftsLoaded) + ? state.availableShifts + : []; + final List pendingAssignments = (state is ShiftsLoaded) + ? state.pendingShifts + : []; + final List cancelledShifts = (state is ShiftsLoaded) + ? state.cancelledShifts + : []; + final List historyShifts = (state is ShiftsLoaded) + ? state.historyShifts + : []; - // Filter logic - final filteredJobs = availableJobs.where((s) { - final matchesSearch = - s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || - s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || - s.clientName.toLowerCase().contains(_searchQuery.toLowerCase()); - - if (!matchesSearch) return false; - - if (_jobType == 'all') return true; - if (_jobType == 'one-day') { - return s.durationDays == null || s.durationDays! <= 1; - } - if (_jobType == 'multi-day') return s.durationDays != null && s.durationDays! > 1; - return true; - }).toList(); - - final calendarDays = _getCalendarDays(); - final weekStartDate = calendarDays.first; - final weekEndDate = calendarDays.last; - - final visibleMyShifts = myShifts.where((s) { - try { - final date = DateTime.parse(s.date); - return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) && - date.isBefore(weekEndDate.add(const Duration(days: 1))); - } catch (_) { - return false; - } - }).toList(); - - final visibleCancelledShifts = cancelledShifts.where((s) { - try { - final date = DateTime.parse(s.date); - return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) && - date.isBefore(weekEndDate.add(const Duration(days: 1))); - } catch (_) { - return false; - } - }).toList(); + // Note: "filteredJobs" logic moved to FindShiftsTab + // Note: Calendar logic moved to MyShiftsTab return Scaffold( backgroundColor: AppColors.krowBackground, @@ -161,326 +77,58 @@ class _ShiftsPageState extends State { 20, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Shifts", - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: const Center( - child: Icon(UiIcons.user, size: 20, color: Colors.white), - ), - ), - ], + const Text( + "Shifts", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - const SizedBox(height: 16), + // Tabs Row( children: [ - _buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length), + _buildTab( + "myshifts", + "My Shifts", + UiIcons.calendar, + myShifts.length, + ), const SizedBox(width: 8), - _buildTab("find", "Find Shifts", UiIcons.search, filteredJobs.length), + _buildTab( + "find", + "Find Shifts", + UiIcons.search, + availableJobs.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs. + ), const SizedBox(width: 8), - _buildTab("history", "History", UiIcons.clock, historyShifts.length), + _buildTab( + "history", + "History", + UiIcons.clock, + historyShifts.length, + ), ], ), ], ), ), - // Calendar Selector - if (_activeTab == 'myshifts') - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon(UiIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal), - onPressed: () => setState(() => _weekOffset--), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - ), - Text( - DateFormat('MMMM yyyy').format(weekStartDate), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - IconButton( - icon: const Icon(UiIcons.chevronRight, size: 20, color: AppColors.krowCharcoal), - onPressed: () => setState(() => _weekOffset++), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - ), - ], - ), - ), - // Days Grid - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: calendarDays.map((date) { - final isSelected = _isSameDay(date, _selectedDate); - final dateStr = DateFormat('yyyy-MM-dd').format(date); - final hasShifts = myShifts.any((s) { - try { - return _isSameDay(DateTime.parse(s.date), date); - } catch (_) { return false; } - }); - - return GestureDetector( - onTap: () => setState(() => _selectedDate = date), - child: Column( - children: [ - Container( - width: 44, - height: 60, - decoration: BoxDecoration( - color: isSelected ? AppColors.krowBlue : Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected ? AppColors.krowBlue : AppColors.krowBorder, - width: 1, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - date.day.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isSelected ? Colors.white : AppColors.krowCharcoal, - ), - ), - Text( - DateFormat('E').format(date), - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w500, - color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted, - ), - ), - if (hasShifts && !isSelected) - Container( - margin: const EdgeInsets.only(top: 4), - width: 4, - height: 4, - decoration: const BoxDecoration( - color: AppColors.krowBlue, - shape: BoxShape.circle, - ), - ), - ], - ), - ), - ], - ), - ); - }).toList(), - ), - ], - ), - ), - - if (_activeTab == 'myshifts') - const Divider(height: 1, color: AppColors.krowBorder), - - // Search and Filters for Find Tab (Fixed at top) - if (_activeTab == 'find') - Container( - color: Colors.white, - padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), - child: Column( - children: [ - // Search Bar - Row( - children: [ - Expanded( - child: Container( - height: 48, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE2E8F0)), - ), - child: Row( - children: [ - const Icon(UiIcons.search, size: 20, color: Color(0xFF94A3B8)), - const SizedBox(width: 10), - Expanded( - child: TextField( - onChanged: (v) => setState(() => _searchQuery = v), - decoration: const InputDecoration( - border: InputBorder.none, - hintText: "Search jobs, location...", - hintStyle: TextStyle( - color: Color(0xFF94A3B8), - fontSize: 14, - ), - ), - ), - ), - ], - ), - ), - ), - const SizedBox(width: 8), - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE2E8F0)), - ), - child: const Icon(UiIcons.filter, size: 18, color: Color(0xFF64748B)), - ), - ], - ), - const SizedBox(height: 16), - // Filter Tabs - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildFilterTab('all', 'All Jobs'), - const SizedBox(width: 8), - _buildFilterTab('one-day', 'One Day'), - const SizedBox(width: 8), - _buildFilterTab('multi-day', 'Multi-Day'), - const SizedBox(width: 8), - _buildFilterTab('long-term', 'Long Term'), - ], - ), - ), - ], - ), - ), - // Body Content Expanded( - child: state is ShiftsLoading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - const SizedBox(height: 20), - if (_activeTab == 'myshifts') ...[ - if (pendingAssignments.isNotEmpty) ...[ - _buildSectionHeader("Awaiting Confirmation", const Color(0xFFF59E0B)), - ...pendingAssignments.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ShiftAssignmentCard( - shift: shift, - onConfirm: () => _confirmShift(shift.id), - onDecline: () => _declineShift(shift.id), - isConfirming: true, - ), - )), - const SizedBox(height: 12), - ], - - if (visibleCancelledShifts.isNotEmpty) ...[ - _buildSectionHeader("Cancelled Shifts", AppColors.krowMuted), - ...visibleCancelledShifts.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: _buildCancelledCard( - title: shift.title, - client: shift.clientName, - pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}", - rate: "\$${shift.hourlyRate}/hr · 8h", - date: _formatDateStr(shift.date), - time: "${shift.startTime} - ${shift.endTime}", - address: shift.locationAddress, - isLastMinute: true, - onTap: () {} - ), - )), - const SizedBox(height: 12), - ], - - // Confirmed Shifts - if (visibleMyShifts.isNotEmpty) ...[ - _buildSectionHeader("Confirmed Shifts", AppColors.krowMuted), - ...visibleMyShifts.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard(shift: shift), - )), - ], - - if (visibleMyShifts.isEmpty && pendingAssignments.isEmpty && cancelledShifts.isEmpty) - _buildEmptyState(UiIcons.calendar, "No shifts this week", "Try finding new jobs in the Find tab", null, null), - ], - - if (_activeTab == 'find') ...[ - if (filteredJobs.isEmpty) - _buildEmptyState(UiIcons.search, "No jobs available", "Check back later", null, null) - else - ...filteredJobs.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard( - shift: shift, - onAccept: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Booked!'), - backgroundColor: Color(0xFF10B981), - ), - ); - }, - onDecline: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - }, - ), - )), - ], - - if (_activeTab == 'history') ...[ - if (historyShifts.isEmpty) - _buildEmptyState(UiIcons.clock, "No shift history", "Completed shifts appear here", null, null) - else - ...historyShifts.map((shift) => Padding( - padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard( - shift: shift, - historyMode: true, - ), - )), - ], - - const SizedBox(height: 40), - ], - ), - ), + child: state is ShiftsLoading + ? const Center(child: CircularProgressIndicator()) + : _buildTabContent( + myShifts, + pendingAssignments, + cancelledShifts, + availableJobs, + historyShifts, + ), ), ], ), @@ -490,62 +138,33 @@ class _ShiftsPageState extends State { ); } - String _formatDateStr(String dateStr) { - try { - final date = DateTime.parse(dateStr); - final now = DateTime.now(); - if (_isSameDay(date, now)) return "Today"; - final tomorrow = now.add(const Duration(days: 1)); - if (_isSameDay(date, tomorrow)) return "Tomorrow"; - return DateFormat('EEE, MMM d').format(date); - } catch (_) { - return dateStr; + Widget _buildTabContent( + List myShifts, + List pendingAssignments, + List cancelledShifts, + List availableJobs, + List historyShifts, + ) { + switch (_activeTab) { + case 'myshifts': + return MyShiftsTab( + myShifts: myShifts, + pendingAssignments: pendingAssignments, + cancelledShifts: cancelledShifts, + ); + case 'find': + return FindShiftsTab( + availableJobs: availableJobs, + ); + case 'history': + return HistoryShiftsTab( + historyShifts: historyShifts, + ); + default: + return const SizedBox.shrink(); } } - Widget _buildSectionHeader(String title, Color dotColor) { - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - children: [ - Container(width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle)), - const SizedBox(width: 8), - Text(title, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: dotColor == AppColors.krowMuted ? AppColors.krowMuted : dotColor - )), - ], - ), - ); - } - - Widget _buildFilterTab(String id, String label) { - final isSelected = _jobType == id; - return GestureDetector( - onTap: () => setState(() => _jobType = id), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: isSelected ? AppColors.krowBlue : Colors.white, - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0), - ), - ), - child: Text( - label, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: isSelected ? Colors.white : const Color(0xFF64748B), - ), - ), - ), - ); - } - Widget _buildTab(String id, String label, IconData icon, int count) { final isActive = _activeTab == id; return Expanded( @@ -554,112 +173,57 @@ class _ShiftsPageState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.white.withAlpha((0.2 * 255).round()), - borderRadius: BorderRadius.circular(8), + color: isActive + ? Colors.white + : Colors.white.withAlpha((0.2 * 255).round()), + borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 14, color: isActive ? AppColors.krowBlue : Colors.white), - const SizedBox(width: 6), - Flexible(child: Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isActive ? AppColors.krowBlue : Colors.white), overflow: TextOverflow.ellipsis)), - const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - constraints: const BoxConstraints(minWidth: 18), - decoration: BoxDecoration( - color: isActive ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) : Colors.white.withAlpha((0.2 * 255).round()), - borderRadius: BorderRadius.circular(999), + Icon( + icon, + size: 14, + color: isActive ? AppColors.krowBlue : Colors.white, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isActive ? AppColors.krowBlue : Colors.white, ), - child: Center(child: Text("$count", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isActive ? AppColors.krowBlue : Colors.white))), - ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + constraints: const BoxConstraints(minWidth: 18), + decoration: BoxDecoration( + color: isActive + ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) + : Colors.white.withAlpha((0.2 * 255).round()), + borderRadius: BorderRadius.circular(999), + ), + child: Center( + child: Text( + "$count", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: isActive ? AppColors.krowBlue : Colors.white, + ), + ), + ), + ), ], ), ), ), ); } - - Widget _buildEmptyState(IconData icon, String title, String subtitle, String? actionLabel, VoidCallback? onAction) { - return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 64), child: Column(children: [ - Container(width: 64, height: 64, decoration: BoxDecoration(color: const Color(0xFFF1F3F5), borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 32, color: AppColors.krowMuted)), - const SizedBox(height: 16), - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.krowCharcoal)), - const SizedBox(height: 4), - Text(subtitle, style: const TextStyle(fontSize: 14, color: AppColors.krowMuted)), - if (actionLabel != null && onAction != null) ...[ - const SizedBox(height: 16), - ElevatedButton(onPressed: onAction, style: ElevatedButton.styleFrom(backgroundColor: AppColors.krowBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), child: Text(actionLabel)), - ] - ]))); - } - - Widget _buildCancelledCard({required String title, required String client, required String pay, required String rate, required String date, required String time, required String address, required bool isLastMinute, required VoidCallback onTap}) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.krowBorder) - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFEF4444), shape: BoxShape.circle)), - const SizedBox(width: 6), - const Text("CANCELLED", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Color(0xFFEF4444))), - if (isLastMinute) ...[ - const SizedBox(width: 4), - const Text("• 4hr compensation", style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF10B981))) - ] - ]), - const SizedBox(height: 12), - Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: AppColors.krowBlue.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20)) - ), - const SizedBox(width: 12), - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowCharcoal)), - Text(client, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)) - ])), - Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(pay, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.krowCharcoal)), - Text(rate, style: const TextStyle(fontSize: 10, color: AppColors.krowMuted)) - ]) - ]), - const SizedBox(height: 8), - Row(children: [ - const Icon(LucideIcons.calendar, size: 12, color: AppColors.krowMuted), - const SizedBox(width: 4), - Text(date, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)), - const SizedBox(width: 12), - const Icon(LucideIcons.clock, size: 12, color: AppColors.krowMuted), - const SizedBox(width: 4), - Text(time, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)) - ]), - const SizedBox(height: 4), - Row(children: [ - const Icon(LucideIcons.mapPin, size: 12, color: AppColors.krowMuted), - const SizedBox(width: 4), - Expanded(child: Text(address, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted), overflow: TextOverflow.ellipsis)) - ]), - ])), - ]), - ]), - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart new file mode 100644 index 00000000..7f98111b --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/styles/shifts_styles.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; + +class AppColors { + static const Color krowBlue = UiColors.primary; + static const Color krowYellow = Color(0xFFFFED4A); + static const Color krowCharcoal = UiColors.textPrimary; + static const Color krowMuted = UiColors.textSecondary; + static const Color krowBorder = UiColors.border; + static const Color krowBackground = UiColors.background; + static const Color white = Colors.white; + static const Color black = Colors.black; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart new file mode 100644 index 00000000..32bfdcd4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shared/empty_state_view.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../../styles/shifts_styles.dart'; + +class EmptyStateView extends StatelessWidget { + final IconData icon; + final String title; + final String subtitle; + final String? actionLabel; + final VoidCallback? onAction; + + const EmptyStateView({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + this.actionLabel, + this.onAction, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 64), + child: Column( + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFF1F3F5), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, size: 32, color: AppColors.krowMuted), + ), + const SizedBox(height: 16), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.krowCharcoal, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle(fontSize: 14, color: AppColors.krowMuted), + ), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: onAction, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.krowBlue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(actionLabel!), + ), + ], + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart new file mode 100644 index 00000000..3f9fb459 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../styles/shifts_styles.dart'; +import '../my_shift_card.dart'; +import '../shared/empty_state_view.dart'; + +class FindShiftsTab extends StatefulWidget { + final List availableJobs; + + const FindShiftsTab({ + super.key, + required this.availableJobs, + }); + + @override + State createState() => _FindShiftsTabState(); +} + +class _FindShiftsTabState extends State { + String _searchQuery = ''; + String _jobType = 'all'; + + Widget _buildFilterTab(String id, String label) { + final isSelected = _jobType == id; + return GestureDetector( + onTap: () => setState(() => _jobType = id), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? AppColors.krowBlue : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0), + ), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : const Color(0xFF64748B), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + // Filter logic + final filteredJobs = widget.availableJobs.where((s) { + final matchesSearch = + s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || + s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || + s.clientName.toLowerCase().contains(_searchQuery.toLowerCase()); + + if (!matchesSearch) return false; + + if (_jobType == 'all') return true; + if (_jobType == 'one-day') { + return s.durationDays == null || s.durationDays! <= 1; + } + if (_jobType == 'multi-day') + return s.durationDays != null && s.durationDays! > 1; + return true; + }).toList(); + + return Column( + children: [ + // Search and Filters + Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), + child: Column( + children: [ + // Search Bar + Row( + children: [ + Expanded( + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE2E8F0), + ), + ), + child: Row( + children: [ + const Icon( + UiIcons.search, + size: 20, + color: Color(0xFF94A3B8), + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + onChanged: (v) => + setState(() => _searchQuery = v), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: "Search jobs, location...", + hintStyle: TextStyle( + color: Color(0xFF94A3B8), + fontSize: 14, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE2E8F0), + ), + ), + child: const Icon( + UiIcons.filter, + size: 18, + color: Color(0xFF64748B), + ), + ), + ], + ), + const SizedBox(height: 16), + // Filter Tabs + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterTab('all', 'All Jobs'), + const SizedBox(width: 8), + _buildFilterTab('one-day', 'One Day'), + const SizedBox(width: 8), + _buildFilterTab('multi-day', 'Multi-Day'), + const SizedBox(width: 8), + _buildFilterTab('long-term', 'Long Term'), + ], + ), + ), + ], + ), + ), + + Expanded( + child: filteredJobs.isEmpty + ? EmptyStateView( + icon: UiIcons.search, + title: "No jobs available", + subtitle: "Check back later", + ) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + ...filteredJobs.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MyShiftCard( + shift: shift, + onAccept: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift Booked!'), + backgroundColor: Color(0xFF10B981), + ), + ); + }, + onDecline: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift Declined'), + backgroundColor: Color(0xFFEF4444), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart new file mode 100644 index 00000000..5edb6eff --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../my_shift_card.dart'; +import '../shared/empty_state_view.dart'; + +class HistoryShiftsTab extends StatelessWidget { + final List historyShifts; + + const HistoryShiftsTab({ + super.key, + required this.historyShifts, + }); + + @override + Widget build(BuildContext context) { + if (historyShifts.isEmpty) { + return EmptyStateView( + icon: UiIcons.clock, + title: "No shift history", + subtitle: "Completed shifts appear here", + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + ...historyShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MyShiftCard( + shift: shift, + historyMode: true, + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart new file mode 100644 index 00000000..ef1a5523 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/my_shifts_tab.dart @@ -0,0 +1,582 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/shifts/shifts_bloc.dart'; +import '../my_shift_card.dart'; +import '../shift_assignment_card.dart'; +import '../shared/empty_state_view.dart'; +import '../../styles/shifts_styles.dart'; + +class MyShiftsTab extends StatefulWidget { + final List myShifts; + final List pendingAssignments; + final List cancelledShifts; + + const MyShiftsTab({ + super.key, + required this.myShifts, + required this.pendingAssignments, + required this.cancelledShifts, + }); + + @override + State createState() => _MyShiftsTabState(); +} + +class _MyShiftsTabState extends State { + DateTime _selectedDate = DateTime.now(); + int _weekOffset = 0; + + List _getCalendarDays() { + final now = DateTime.now(); + int reactDayIndex = now.weekday == 7 ? 0 : now.weekday; + int daysSinceFriday = (reactDayIndex + 2) % 7; + final start = now + .subtract(Duration(days: daysSinceFriday)) + .add(Duration(days: _weekOffset * 7)); + final startDate = DateTime(start.year, start.month, start.day); + return List.generate(7, (index) => startDate.add(Duration(days: index))); + } + + bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + void _confirmShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Accept Shift'), + content: const Text( + 'Are you sure you want to accept this shift?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(AcceptShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift confirmed!'), + backgroundColor: Color(0xFF10B981), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + ), + child: const Text('Accept'), + ), + ], + ), + ); + } + + void _declineShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(DeclineShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift declined.'), + backgroundColor: Color(0xFFEF4444), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + ), + child: const Text('Decline'), + ), + ], + ), + ); + } + + String _formatDateStr(String dateStr) { + try { + final date = DateTime.parse(dateStr); + final now = DateTime.now(); + if (_isSameDay(date, now)) return "Today"; + final tomorrow = now.add(const Duration(days: 1)); + if (_isSameDay(date, tomorrow)) return "Tomorrow"; + return DateFormat('EEE, MMM d').format(date); + } catch (_) { + return dateStr; + } + } + + @override + Widget build(BuildContext context) { + final calendarDays = _getCalendarDays(); + final weekStartDate = calendarDays.first; + final weekEndDate = calendarDays.last; + + final visibleMyShifts = widget.myShifts.where((s) { + try { + final date = DateTime.parse(s.date); + return date.isAfter( + weekStartDate.subtract(const Duration(seconds: 1)), + ) && + date.isBefore(weekEndDate.add(const Duration(days: 1))); + } catch (_) { + return false; + } + }).toList(); + + final visibleCancelledShifts = widget.cancelledShifts.where((s) { + try { + final date = DateTime.parse(s.date); + return date.isAfter( + weekStartDate.subtract(const Duration(seconds: 1)), + ) && + date.isBefore(weekEndDate.add(const Duration(days: 1))); + } catch (_) { + return false; + } + }).toList(); + + return Column( + children: [ + // Calendar Selector + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + UiIcons.chevronLeft, + size: 20, + color: AppColors.krowCharcoal, + ), + onPressed: () => setState(() => _weekOffset--), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + ), + Text( + DateFormat('MMMM yyyy').format(weekStartDate), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.krowCharcoal, + ), + ), + IconButton( + icon: const Icon( + UiIcons.chevronRight, + size: 20, + color: AppColors.krowCharcoal, + ), + onPressed: () => setState(() => _weekOffset++), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + ), + // Days Grid + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: calendarDays.map((date) { + final isSelected = _isSameDay(date, _selectedDate); + // ignore: unused_local_variable + final dateStr = DateFormat('yyyy-MM-dd').format(date); + final hasShifts = widget.myShifts.any((s) { + try { + return _isSameDay(DateTime.parse(s.date), date); + } catch (_) { + return false; + } + }); + + return GestureDetector( + onTap: () => setState(() => _selectedDate = date), + child: Column( + children: [ + Container( + width: 44, + height: 60, + decoration: BoxDecoration( + color: isSelected ? AppColors.krowBlue : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? AppColors.krowBlue + : AppColors.krowBorder, + width: 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isSelected + ? Colors.white + : AppColors.krowCharcoal, + ), + ), + Text( + DateFormat('E').format(date), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: isSelected + ? Colors.white.withOpacity(0.8) + : AppColors.krowMuted, + ), + ), + if (hasShifts && !isSelected) + Container( + margin: const EdgeInsets.only(top: 4), + width: 4, + height: 4, + decoration: const BoxDecoration( + color: AppColors.krowBlue, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ), + ), + const Divider(height: 1, color: AppColors.krowBorder), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + if (widget.pendingAssignments.isNotEmpty) ...[ + _buildSectionHeader( + "Awaiting Confirmation", + const Color(0xFFF59E0B), + ), + ...widget.pendingAssignments.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ShiftAssignmentCard( + shift: shift, + onConfirm: () => _confirmShift(shift.id), + onDecline: () => _declineShift(shift.id), + isConfirming: true, + ), + ), + ), + const SizedBox(height: 12), + ], + + if (visibleCancelledShifts.isNotEmpty) ...[ + _buildSectionHeader( + "Cancelled Shifts", + AppColors.krowMuted, + ), + ...visibleCancelledShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _buildCancelledCard( + title: shift.title, + client: shift.clientName, + pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}", + rate: "\$${shift.hourlyRate}/hr · 8h", + date: _formatDateStr(shift.date), + time: "${shift.startTime} - ${shift.endTime}", + address: shift.locationAddress, + isLastMinute: true, + onTap: () {}, + ), + ), + ), + const SizedBox(height: 12), + ], + + // Confirmed Shifts + if (visibleMyShifts.isNotEmpty) ...[ + _buildSectionHeader( + "Confirmed Shifts", + AppColors.krowMuted, + ), + ...visibleMyShifts.map( + (shift) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MyShiftCard(shift: shift), + ), + ), + ], + + if (visibleMyShifts.isEmpty && + widget.pendingAssignments.isEmpty && + widget.cancelledShifts.isEmpty) + const EmptyStateView( + icon: UiIcons.calendar, + title: "No shifts this week", + subtitle: "Try finding new jobs in the Find tab", + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ); + } + + Widget _buildSectionHeader(String title, Color dotColor) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle), + ), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: dotColor == AppColors.krowMuted + ? AppColors.krowMuted + : dotColor, + ), + ), + ], + ), + ); + } + + + + Widget _buildCancelledCard({ + required String title, + required String client, + required String pay, + required String rate, + required String date, + required String time, + required String address, + required bool isLastMinute, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.krowBorder), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Color(0xFFEF4444), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + const Text( + "CANCELLED", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Color(0xFFEF4444), + ), + ), + if (isLastMinute) ...[ + const SizedBox(width: 4), + const Text( + "• 4hr compensation", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Color(0xFF10B981), + ), + ), + ], + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.krowBlue.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon( + LucideIcons.briefcase, + color: AppColors.krowBlue, + size: 20, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.krowCharcoal, + ), + ), + Text( + client, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + pay, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.krowCharcoal, + ), + ), + Text( + rate, + style: const TextStyle( + fontSize: 10, + color: AppColors.krowMuted, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + LucideIcons.calendar, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Text( + date, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + const SizedBox(width: 12), + const Icon( + LucideIcons.clock, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Text( + time, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + const Icon( + LucideIcons.mapPin, + size: 12, + color: AppColors.krowMuted, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + address, + style: const TextStyle( + fontSize: 12, + color: AppColors.krowMuted, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} From eac6c1b7784bdae9084ca610d12d2a2423ce22f4 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 19:47:29 -0500 Subject: [PATCH 03/13] feat: Enhance shift application process with instant booking option and implement shift booking and decline dialogs --- .../shifts_repository_impl.dart | 20 +- .../shifts_repository_interface.dart | 4 +- .../usecases/apply_for_shift_usecase.dart | 11 + .../blocs/shifts/shifts_bloc.dart | 16 + .../blocs/shifts/shifts_event.dart | 8 + .../pages/shift_details_page.dart | 869 ------------------ .../widgets/tabs/find_shifts_tab.dart | 88 +- .../shifts/lib/src/staff_shifts_module.dart | 2 + 8 files changed, 130 insertions(+), 888 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart delete mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 584a4bcd..6f76b2a7 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -223,7 +223,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } @override - Future applyForShift(String shiftId) async { + Future applyForShift(String shiftId, {bool isInstantBook = false}) async { final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift'); @@ -234,7 +234,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { shiftId: shiftId, staffId: staffId, roleId: role.id, - status: dc.ApplicationStatus.PENDING, + status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING, origin: dc.ApplicationOrigin.STAFF, ).execute(); } @@ -273,6 +273,22 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } if (appId == null || roleId == null) { + // If we are rejecting and can't find an application, create one as rejected (declining an available shift) + if (newStatus == dc.ApplicationStatus.REJECTED) { + final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); + if (rolesResult.data.shiftRoles.isNotEmpty) { + final role = rolesResult.data.shiftRoles.first; + final staffId = await _getStaffId(); + await _dataConnect.createApplication( + shiftId: shiftId, + staffId: staffId, + roleId: role.id, + status: dc.ApplicationStatus.REJECTED, + origin: dc.ApplicationOrigin.STAFF, + ).execute(); + return; + } + } throw Exception("Application not found for shift $shiftId"); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart index f77844e5..c3767fd0 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart @@ -18,7 +18,9 @@ abstract interface class ShiftsRepositoryInterface { Future getShiftDetails(String shiftId); /// Applies for a specific open shift. - Future applyForShift(String shiftId); + /// + /// [isInstantBook] determines if the application should be immediately accepted. + Future applyForShift(String shiftId, {bool isInstantBook = false}); /// Accepts a pending shift assignment. Future acceptShift(String shiftId); diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart new file mode 100644 index 00000000..a637be4c --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart @@ -0,0 +1,11 @@ +import '../repositories/shifts_repository_interface.dart'; + +class ApplyForShiftUseCase { + final ShiftsRepositoryInterface repository; + + ApplyForShiftUseCase(this.repository); + + Future call(String shiftId, {bool isInstantBook = false}) async { + return repository.applyForShift(shiftId, isInstantBook: isInstantBook); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart index d2f26c17..0fb6f979 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -11,6 +11,7 @@ import '../../../domain/usecases/get_cancelled_shifts_usecase.dart'; import '../../../domain/usecases/get_history_shifts_usecase.dart'; import '../../../domain/usecases/accept_shift_usecase.dart'; import '../../../domain/usecases/decline_shift_usecase.dart'; +import '../../../domain/usecases/apply_for_shift_usecase.dart'; part 'shifts_event.dart'; part 'shifts_state.dart'; @@ -23,6 +24,7 @@ class ShiftsBloc extends Bloc { final GetHistoryShiftsUseCase getHistoryShifts; final AcceptShiftUseCase acceptShift; final DeclineShiftUseCase declineShift; + final ApplyForShiftUseCase applyForShift; ShiftsBloc({ required this.getMyShifts, @@ -32,11 +34,13 @@ class ShiftsBloc extends Bloc { required this.getHistoryShifts, required this.acceptShift, required this.declineShift, + required this.applyForShift, }) : super(ShiftsInitial()) { on(_onLoadShifts); on(_onFilterAvailableShifts); on(_onAcceptShift); on(_onDeclineShift); + on(_onBookShift); } Future _onLoadShifts( @@ -122,4 +126,16 @@ class ShiftsBloc extends Bloc { // Handle error } } + + Future _onBookShift( + BookShiftEvent event, + Emitter emit, + ) async { + try { + await applyForShift(event.shiftId, isInstantBook: true); + add(LoadShiftsEvent()); // Reload to move from Available to My Shifts + } catch (_) { + // Handle error + } + } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart index 0ea1a6b1..eeab5787 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart @@ -35,3 +35,11 @@ class DeclineShiftEvent extends ShiftsEvent { @override List get props => [shiftId]; } + +class BookShiftEvent extends ShiftsEvent { + final String shiftId; + const BookShiftEvent(this.shiftId); + + @override + List get props => [shiftId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart deleted file mode 100644 index c62b3b15..00000000 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ /dev/null @@ -1,869 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:lucide_icons/lucide_icons.dart'; -import 'package:intl/intl.dart'; -import 'package:design_system/design_system.dart'; -import 'package:krow_domain/krow_domain.dart'; -import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart'; -import '../../domain/usecases/get_shift_details_usecase.dart'; -import '../../domain/usecases/accept_shift_usecase.dart'; -import '../../domain/usecases/decline_shift_usecase.dart'; - -// Shim to match POC styles locally -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = UiColors.textPrimary; // 121826 - static const Color krowMuted = UiColors.textSecondary; // 6A7382 - static const Color krowBorder = UiColors.border; // E3E6E9 - static const Color krowBackground = UiColors.background; // FAFBFC - static const Color white = Colors.white; -} - -class ShiftDetailsPage extends StatefulWidget { - final String shiftId; - final Shift? shift; - - const ShiftDetailsPage({super.key, required this.shiftId, this.shift}); - - @override - State createState() => _ShiftDetailsPageState(); -} - -class _ShiftDetailsPageState extends State { - late Shift _shift; - bool _isLoading = true; - bool _showDetails = true; - bool _isApplying = false; - - - - @override - void initState() { - super.initState(); - _loadShift(); - } - - void _loadShift() async { - if (widget.shift != null) { - _shift = widget.shift!; - setState(() => _isLoading = false); - } else { - try { - final useCase = Modular.get(); - final shift = await useCase(widget.shiftId); - if (mounted) { - if (shift != null) { - setState(() { - _shift = shift; - _isLoading = false; - }); - } else { - // Handle case where shift is not found - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Shift not found')), - ); - } - } - } catch (e) { - if (mounted) { - setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error loading shift: $e')), - ); - } - } - } - } - - String _formatTime(String time) { - if (time.isEmpty) return ''; - try { - final parts = time.split(':'); - final hour = int.parse(parts[0]); - final minute = int.parse(parts[1]); - final dt = DateTime(2022, 1, 1, hour, minute); - return DateFormat('h:mma').format(dt).toLowerCase(); - } catch (e) { - return time; - } - } - - String _formatDate(String dateStr) { - if (dateStr.isEmpty) return ''; - try { - final date = DateTime.parse(dateStr); - return DateFormat('MMMM d').format(date); - } catch (e) { - return dateStr; - } - } - - double _calculateHours(String start, String end) { - try { - final startParts = start.split(':').map(int.parse).toList(); - final endParts = end.split(':').map(int.parse).toList(); - double h = (endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60; - if (h < 0) h += 24; - return h; - } catch (e) { - return 0; - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return const Scaffold( - backgroundColor: AppColors.krowBackground, - body: Center(child: CircularProgressIndicator()), - ); - } - - final hours = _calculateHours(_shift.startTime, _shift.endTime); - final totalPay = _shift.hourlyRate * hours; - - return Scaffold( - backgroundColor: AppColors.krowBackground, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(LucideIcons.chevronLeft, color: AppColors.krowMuted), - onPressed: () => Modular.to.pop(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: AppColors.krowBorder, height: 1.0), - ), - ), - body: Stack( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Pending Badge - // Status Badge - Align( - alignment: Alignment.centerRight, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: _getStatusColor(_shift.status ?? 'open').withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - (_shift.status ?? 'open').toUpperCase(), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: _getStatusColor(_shift.status ?? 'open'), - ), - ), - ), - ), - const SizedBox(height: 16), - - // Header - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: _shift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - _shift.logoUrl!, - fit: BoxFit.contain, - ), - ) - : Center( - child: Text( - _shift.clientName.isNotEmpty ? _shift.clientName[0] : 'K', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: AppColors.krowBlue, - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _shift.title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '\$${_shift.hourlyRate.toStringAsFixed(0)}/h', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - '(exp.total \$${totalPay.toStringAsFixed(0)})', - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - Text( - _shift.clientName, - style: const TextStyle(color: AppColors.krowMuted), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - - - - // Additional Details Collapsible - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - children: [ - InkWell( - onTap: () => - setState(() => _showDetails = !_showDetails), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'ADDITIONAL DETAILS', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - color: AppColors.krowMuted, - ), - ), - Icon( - _showDetails - ? LucideIcons.chevronUp - : LucideIcons.chevronDown, - color: AppColors.krowMuted, - size: 20, - ), - ], - ), - ), - ), - if (_showDetails) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - children: [ - _buildDetailRow('Tips', _shift.tipsAvailable == true ? 'Yes' : 'No', _shift.tipsAvailable == true), - _buildDetailRow('Travel Time', _shift.travelTime == true ? 'Yes' : 'No', _shift.travelTime == true), - _buildDetailRow('Meal Provided', _shift.mealProvided == true ? 'Yes' : 'No', _shift.mealProvided == true), - _buildDetailRow('Parking Available', _shift.parkingAvailable == true ? 'Yes' : 'No', _shift.parkingAvailable == true), - _buildDetailRow('Gas Compensation', _shift.gasCompensation == true ? 'Yes' : 'No', _shift.gasCompensation == true), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Date & Duration Grid - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'START', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 8), - Text( - _formatDate(_shift.date), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Text( - _formatTime(_shift.startTime), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Time', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'DURATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 8), - Text( - '${hours.toStringAsFixed(0)} hours', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Shift duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - const Text( - '1 hour', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Break duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Location - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'LOCATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _shift.location, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - _shift.locationAddress, - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - _shift.locationAddress, - ), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon(LucideIcons.navigation, size: 14), - label: const Text('Get direction'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.krowCharcoal, - side: const BorderSide( - color: AppColors.krowBorder, - ), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ], - ), - const SizedBox(height: 16), - Container( - height: 160, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFF1F3F5), - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon( - LucideIcons.map, - color: AppColors.krowMuted, - size: 48, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Manager Contact - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'MANAGER CONTACT DETAILS', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 16), - ...(_shift.managers ?? []) - .map( - (manager) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - AppColors.krowBlue, - Color(0xFF0830B8), - ], - ), - borderRadius: BorderRadius.circular( - 8, - ), - ), - child: _buildAvatar(manager), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - manager.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - Text( - manager.phone, - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text(manager.phone), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon( - LucideIcons.phone, - size: 14, - color: Color(0xFF059669), - ), - label: const Text( - 'Call', - style: TextStyle( - color: Color(0xFF059669), - ), - ), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Color(0xFFA7F3D0), - ), - backgroundColor: const Color(0xFFECFDF5), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ], - ), - ), - ) - .toList(), - ], - ), - ), - const SizedBox(height: 16), - - // Additional Info - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'ADDITIONAL INFO', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Text( - _shift.description ?? - 'Providing Exceptional Customer Service.', - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - height: 1.5, - ), - ), - ], - ), - ), - ], - ), - ), - - // Bottom Actions - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(20), - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: AppColors.krowBorder)), - ), - child: SafeArea( - top: false, - child: Column( - children: [ - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () async { - setState(() => _isApplying = true); - try { - final acceptUseCase = Modular.get(); - await acceptUseCase(_shift.id); - - if (mounted) { - setState(() => _isApplying = false); - Modular.to.pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Accepted!'), - backgroundColor: Color(0xFF10B981), - ), - ); - // Ideally, trigger a refresh on the previous screen - Modular.get().add(LoadShiftsEvent()); - } - } catch (e) { - if (mounted) { - setState(() => _isApplying = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to accept shift: $e'), - backgroundColor: const Color(0xFFEF4444), - ), - ); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.krowBlue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: _isApplying - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : const Text( - 'Accept shift', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 48, - child: TextButton( - onPressed: () async { - try { - final declineUseCase = Modular.get(); - await declineUseCase(_shift.id); - - if (mounted) { - Modular.to.pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - // Refresh list - Modular.get().add(LoadShiftsEvent()); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to decline shift: $e'), - backgroundColor: const Color(0xFFEF4444), - ), - ); - } - } - }, - child: const Text( - 'Decline shift', - style: TextStyle( - color: Color(0xFFEF4444), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTag(IconData icon, String label, Color bg, Color text) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - Icon(icon, size: 14, color: text), - const SizedBox(width: 4), - Text( - label, - style: TextStyle( - color: text, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); - } - - Color _getStatusColor(String status) { - switch (status.toLowerCase()) { - case 'confirmed': - case 'accepted': - return const Color(0xFF10B981); // Green - case 'pending': - return const Color(0xFFF59E0B); // Yellow - case 'cancelled': - case 'rejected': - return const Color(0xFFEF4444); // Red - case 'completed': - return const Color(0xFF10B981); - default: - return AppColors.krowBlue; - } - } - - Widget _buildAvatar(ShiftManager manager) { - if (manager.avatar != null && manager.avatar!.isNotEmpty) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network(manager.avatar!, fit: BoxFit.cover), - ); - } - return const Center( - child: Icon( - LucideIcons.user, - color: Colors.white, - size: 20, - ), - ); - } - - Widget _buildDetailRow(String label, String value, bool isPositive) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: const TextStyle(fontSize: 14, color: AppColors.krowMuted), - ), - Text( - value, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: isPositive ? const Color(0xFF059669) : AppColors.krowMuted, - ), - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 3f9fb459..215da086 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/shifts/shifts_bloc.dart'; import '../../styles/shifts_styles.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -21,6 +23,74 @@ class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; + void _bookShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Book Shift'), + content: const Text( + 'Do you want to instantly book this shift?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(BookShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift Booking processed!'), + backgroundColor: Color(0xFF10B981), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + ), + child: const Text('Book'), + ), + ], + ), + ); + } + + void _declineShift(String id) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().add(DeclineShiftEvent(id)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Shift Declined'), + backgroundColor: Color(0xFFEF4444), + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + ), + child: const Text('Decline'), + ), + ], + ), + ); + } + Widget _buildFilterTab(String id, String label) { final isSelected = _jobType == id; return GestureDetector( @@ -171,22 +241,8 @@ class _FindShiftsTabState extends State { padding: const EdgeInsets.only(bottom: 12), child: MyShiftCard( shift: shift, - onAccept: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Booked!'), - backgroundColor: Color(0xFF10B981), - ), - ); - }, - onDecline: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - }, + onAccept: () => _bookShift(shift.id), + onDecline: () => _declineShift(shift.id), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart index 8b979692..bb8e9f9a 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -8,6 +8,7 @@ import 'domain/usecases/get_cancelled_shifts_usecase.dart'; import 'domain/usecases/get_history_shifts_usecase.dart'; import 'domain/usecases/accept_shift_usecase.dart'; import 'domain/usecases/decline_shift_usecase.dart'; +import 'domain/usecases/apply_for_shift_usecase.dart'; import 'domain/usecases/get_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; import 'presentation/pages/shifts_page.dart'; @@ -27,6 +28,7 @@ class StaffShiftsModule extends Module { i.add(GetHistoryShiftsUseCase.new); i.add(AcceptShiftUseCase.new); i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); i.add(GetShiftDetailsUseCase.new); // Bloc From c6128c2332117ce1730d5590dec0c3c07ad8b87f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 20:33:35 -0500 Subject: [PATCH 04/13] feat: Integrate Google Maps Places Autocomplete for Hub Address Validation - Refactored ShiftsBloc to remove unused shift-related events and use cases. - Updated navigation paths in ShiftsNavigator to reflect new structure. - Simplified MyShiftCard widget by removing unnecessary parameters and logic. - Modified FindShiftsTab and HistoryShiftsTab to utilize new navigation for shift details. - Created ShiftDetailsModule with necessary bindings and routes for shift details. - Implemented ShiftDetailsBloc, ShiftDetailsEvent, and ShiftDetailsState for managing shift details. - Developed ShiftDetailsPage to display detailed information about a shift and handle booking/declining actions. - Added necessary imports and adjusted existing files to accommodate new shift details functionality. --- .../shift_details/shift_details_bloc.dart | 63 +++ .../shift_details/shift_details_event.dart | 32 ++ .../shift_details/shift_details_state.dart | 37 ++ .../blocs/shifts/shifts_bloc.dart | 54 +-- .../blocs/shifts/shifts_event.dart | 8 - .../navigation/shifts_navigator.dart | 2 +- .../pages/shift_details_page.dart | 433 ++++++++++++++++++ .../presentation/widgets/my_shift_card.dart | 396 +--------------- .../widgets/tabs/find_shifts_tab.dart | 74 +-- .../widgets/tabs/history_shifts_tab.dart | 10 +- .../shifts/lib/src/shift_details_module.dart | 31 ++ .../shifts/lib/src/staff_shifts_module.dart | 3 +- .../staff/shifts/lib/staff_shifts.dart | 2 + .../staff_main/lib/src/staff_main_module.dart | 4 + 14 files changed, 623 insertions(+), 526 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart new file mode 100644 index 00000000..5b225f06 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart @@ -0,0 +1,63 @@ +import 'package:bloc/bloc.dart'; +import '../../../domain/usecases/apply_for_shift_usecase.dart'; +import '../../../domain/usecases/decline_shift_usecase.dart'; +import '../../../domain/usecases/get_shift_details_usecase.dart'; +import 'shift_details_event.dart'; +import 'shift_details_state.dart'; + +class ShiftDetailsBloc extends Bloc { + final GetShiftDetailsUseCase getShiftDetails; + final ApplyForShiftUseCase applyForShift; + final DeclineShiftUseCase declineShift; + + ShiftDetailsBloc({ + required this.getShiftDetails, + required this.applyForShift, + required this.declineShift, + }) : super(ShiftDetailsInitial()) { + on(_onLoadDetails); + on(_onBookShift); + on(_onDeclineShift); + } + + Future _onLoadDetails( + LoadShiftDetailsEvent event, + Emitter emit, + ) async { + emit(ShiftDetailsLoading()); + try { + final shift = await getShiftDetails(event.shiftId); + if (shift != null) { + emit(ShiftDetailsLoaded(shift)); + } else { + emit(const ShiftDetailsError("Shift not found")); + } + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onBookShift( + BookShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await applyForShift(event.shiftId, isInstantBook: true); + emit(const ShiftActionSuccess("Shift successfully booked!")); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onDeclineShift( + DeclineShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await declineShift(event.shiftId); + emit(const ShiftActionSuccess("Shift declined")); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart new file mode 100644 index 00000000..1080065c --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +abstract class ShiftDetailsEvent extends Equatable { + const ShiftDetailsEvent(); + + @override + List get props => []; +} + +class LoadShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const LoadShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} + +class BookShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const BookShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} + +class DeclineShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const DeclineShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart new file mode 100644 index 00000000..b1a239c4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class ShiftDetailsState extends Equatable { + const ShiftDetailsState(); + + @override + List get props => []; +} + +class ShiftDetailsInitial extends ShiftDetailsState {} + +class ShiftDetailsLoading extends ShiftDetailsState {} + +class ShiftDetailsLoaded extends ShiftDetailsState { + final Shift shift; + const ShiftDetailsLoaded(this.shift); + + @override + List get props => [shift]; +} + +class ShiftDetailsError extends ShiftDetailsState { + final String message; + const ShiftDetailsError(this.message); + + @override + List get props => [message]; +} + +class ShiftActionSuccess extends ShiftDetailsState { + final String message; + const ShiftActionSuccess(this.message); + + @override + List get props => [message]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart index 0fb6f979..1b21f68b 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -3,15 +3,12 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:meta/meta.dart'; -import '../../../domain/usecases/get_available_shifts_usecase.dart'; import '../../../domain/arguments/get_available_shifts_arguments.dart'; -import '../../../domain/usecases/get_my_shifts_usecase.dart'; -import '../../../domain/usecases/get_pending_assignments_usecase.dart'; +import '../../../domain/usecases/get_available_shifts_usecase.dart'; import '../../../domain/usecases/get_cancelled_shifts_usecase.dart'; import '../../../domain/usecases/get_history_shifts_usecase.dart'; -import '../../../domain/usecases/accept_shift_usecase.dart'; -import '../../../domain/usecases/decline_shift_usecase.dart'; -import '../../../domain/usecases/apply_for_shift_usecase.dart'; +import '../../../domain/usecases/get_my_shifts_usecase.dart'; +import '../../../domain/usecases/get_pending_assignments_usecase.dart'; part 'shifts_event.dart'; part 'shifts_state.dart'; @@ -22,9 +19,6 @@ class ShiftsBloc extends Bloc { final GetPendingAssignmentsUseCase getPendingAssignments; final GetCancelledShiftsUseCase getCancelledShifts; final GetHistoryShiftsUseCase getHistoryShifts; - final AcceptShiftUseCase acceptShift; - final DeclineShiftUseCase declineShift; - final ApplyForShiftUseCase applyForShift; ShiftsBloc({ required this.getMyShifts, @@ -32,15 +26,9 @@ class ShiftsBloc extends Bloc { required this.getPendingAssignments, required this.getCancelledShifts, required this.getHistoryShifts, - required this.acceptShift, - required this.declineShift, - required this.applyForShift, }) : super(ShiftsInitial()) { on(_onLoadShifts); on(_onFilterAvailableShifts); - on(_onAcceptShift); - on(_onDeclineShift); - on(_onBookShift); } Future _onLoadShifts( @@ -102,40 +90,4 @@ class ShiftsBloc extends Bloc { } } } - - Future _onAcceptShift( - AcceptShiftEvent event, - Emitter emit, - ) async { - try { - await acceptShift(event.shiftId); - add(LoadShiftsEvent()); // Reload lists - } catch (_) { - // Handle error - } - } - - Future _onDeclineShift( - DeclineShiftEvent event, - Emitter emit, - ) async { - try { - await declineShift(event.shiftId); - add(LoadShiftsEvent()); // Reload lists - } catch (_) { - // Handle error - } - } - - Future _onBookShift( - BookShiftEvent event, - Emitter emit, - ) async { - try { - await applyForShift(event.shiftId, isInstantBook: true); - add(LoadShiftsEvent()); // Reload to move from Available to My Shifts - } catch (_) { - // Handle error - } - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart index eeab5787..0ea1a6b1 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart @@ -35,11 +35,3 @@ class DeclineShiftEvent extends ShiftsEvent { @override List get props => [shiftId]; } - -class BookShiftEvent extends ShiftsEvent { - final String shiftId; - const BookShiftEvent(this.shiftId); - - @override - List get props => [shiftId]; -} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart index 4832055b..306caa5a 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart @@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart'; extension ShiftsNavigator on IModularNavigator { void pushShiftDetails(Shift shift) { - pushNamed('/shifts/details/${shift.id}', arguments: shift); + pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift); } // Example for going back or internal navigation if needed diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart new file mode 100644 index 00000000..7ade6d31 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic +import 'package:intl/intl.dart'; +import '../blocs/shift_details/shift_details_bloc.dart'; +import '../blocs/shift_details/shift_details_event.dart'; +import '../blocs/shift_details/shift_details_state.dart'; +import '../styles/shifts_styles.dart'; +import '../widgets/my_shift_card.dart'; + +class ShiftDetailsPage extends StatelessWidget { + final String shiftId; + final Shift? shift; + + const ShiftDetailsPage({ + super.key, + required this.shiftId, + this.shift, + }); + + String _formatTime(String time) { + if (time.isEmpty) return ''; + try { + final parts = time.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + final dt = DateTime(2022, 1, 1, hour, minute); + return DateFormat('h:mm a').format(dt); + } catch (e) { + return time; + } + } + + double _calculateDuration(Shift shift) { + if (shift.startTime.isEmpty || shift.endTime.isEmpty) { + return 0; + } + try { + final s = shift.startTime.split(':').map(int.parse).toList(); + final e = shift.endTime.split(':').map(int.parse).toList(); + double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } catch (_) { + return 0; + } + } + + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: UiColors.border), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: UiColors.iconSecondary), + ), + const SizedBox(height: 8), + Text( + value, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + label, + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + _formatTime(time), + style: UiTypography.display2m.copyWith( + fontSize: 20, + color: UiColors.textPrimary, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => Modular.get() + ..add(LoadShiftDetailsEvent(shiftId)), + child: BlocListener( + listener: (context, state) { + if (state is ShiftActionSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFF10B981), + ), + ); + Modular.to.pop(true); // Return outcome + } else if (state is ShiftDetailsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFFEF4444), + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is ShiftDetailsLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + Shift? displayShift; + if (state is ShiftDetailsLoaded) { + displayShift = state.shift; + } else { + displayShift = shift; + } + + if (displayShift == null) { + return const Scaffold( + body: Center(child: Text("Shift not found")), + ); + } + + final duration = _calculateDuration(displayShift); + final estimatedTotal = (displayShift.hourlyRate) * duration; + + return Scaffold( + backgroundColor: AppColors.krowBackground, + appBar: AppBar( + title: const Text("Shift Details"), + backgroundColor: Colors.white, + foregroundColor: AppColors.krowCharcoal, + elevation: 0.5, + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + MyShiftCard( + shift: displayShift, + // No direct actions on the card, handled by page buttons + ), + const SizedBox(height: 24), + + // Stats Row + Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toInt()}", + "Hourly Rate", + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + UiIcons.clock, + "${duration.toInt()}", + "Hours", + ), + ), + ], + ), + const SizedBox(height: 24), + + // In/Out Time + Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildTimeBox( + "CLOCK OUT TIME", + displayShift.endTime, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "LOCATION", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(displayShift!.locationAddress), + duration: const Duration(seconds: 3), + ), + ); + }, + icon: const Icon(UiIcons.navigation, size: 14), + label: const Text( + "Get direction", + style: TextStyle(fontSize: 12), + ), + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.textPrimary, + side: const BorderSide(color: UiColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + minimumSize: const Size(0, 32), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + height: 128, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon( + UiIcons.mapPin, + color: UiColors.iconSecondary, + size: 32, + ), + ), + // Placeholder for Map + ), + ], + ), + const SizedBox(height: 24), + + // Additional Info + if (displayShift.description != null) ...[ + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ADDITIONAL INFO", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + displayShift.description!, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _declineShift(context, displayShift!.id), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + side: const BorderSide(color: Color(0xFFEF4444)), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Decline"), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => _bookShift(context, displayShift!.id), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Book Shift"), + ), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).padding.bottom + 10), + ], + ), + ), + ); + }, + ), + ), + ); + } + + void _bookShift(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Book Shift'), + content: const Text('Do you want to instantly book this shift?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + BlocProvider.of(context).add(BookShiftDetailsEvent(id)); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + ), + child: const Text('Book'), + ), + ], + ), + ); + } + + void _declineShift(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + BlocProvider.of(context).add(DeclineShiftDetailsEvent(id)); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + ), + child: const Text('Decline'), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index c24fa6c1..7175e004 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -1,25 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:staff_shifts/src/presentation/navigation/shifts_navigator.dart'; class MyShiftCard extends StatefulWidget { final Shift shift; - final bool historyMode; - final VoidCallback? onAccept; - final VoidCallback? onDecline; - final VoidCallback? onRequestSwap; - final int index; const MyShiftCard({ super.key, required this.shift, - this.historyMode = false, - this.onAccept, - this.onDecline, - this.onRequestSwap, - this.index = 0, }); @override @@ -27,8 +19,6 @@ class MyShiftCard extends StatefulWidget { } class _MyShiftCardState extends State { - bool _isExpanded = false; - String _formatTime(String time) { if (time.isEmpty) return ''; try { @@ -120,9 +110,10 @@ class _MyShiftCardState extends State { } return GestureDetector( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), + onTap: () { + Modular.to.pushShiftDetails(widget.shift); + }, + child: Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, @@ -389,384 +380,9 @@ class _MyShiftCardState extends State { ], ), ), - - // Expanded Content - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: _isExpanded - ? Column( - children: [ - const Divider(height: 1, color: UiColors.border), - Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - // Stats Row - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${widget.shift.hourlyRate.toInt()}", - "Hourly Rate", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration.toInt()}", - "Hours", - ), - ), - ], - ), - const SizedBox(height: 24), - - // In/Out Time - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - widget.shift.startTime, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - widget.shift.endTime, - ), - ), - ], - ), - const SizedBox(height: 24), - - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "LOCATION", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.shift.location.isEmpty - ? "TBD" - : widget.shift.location, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - OutlinedButton.icon( - onPressed: () { - // Show snackbar with the address - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - widget.shift.locationAddress, - ), - duration: const Duration( - seconds: 3, - ), - ), - ); - }, - icon: const Icon( - UiIcons.navigation, - size: 14, - ), - label: const Text( - "Get direction", - style: TextStyle(fontSize: 12), - ), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide( - color: UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 20, - ), - ), - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 0, - ), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - height: 128, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon( - UiIcons.mapPin, - color: UiColors.iconSecondary, - size: 32, - ), - ), - // Placeholder for Map - ), - ], - ), - const SizedBox(height: 24), - - // Additional Info - if (widget.shift.description != null) ...[ - SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - "ADDITIONAL INFO", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Text( - widget.shift.description!.split('.')[0], - style: UiTypography.body2m.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - widget.shift.description!, - style: UiTypography.body3r.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - ), - const SizedBox(height: 24), - ], - - // Actions - if (!widget.historyMode) - if (status == 'confirmed') - SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton.icon( - onPressed: widget.onRequestSwap, - icon: const Icon( - UiIcons.swap, - size: 16, - ), - label: Text( - t.staff_shifts.action.request_swap), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.primary, - side: const BorderSide( - color: UiColors.primary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 12, - ), - ), - ), - ), - ) - else if (status == 'swap') - Container( - width: double.infinity, - height: 48, - decoration: BoxDecoration( - color: const Color( - 0xFFFFFBEB, - ), // amber-50 - border: Border.all( - color: const Color(0xFFFDE68A), - ), // amber-200 - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - UiIcons.swap, - size: 16, - color: Color(0xFFB45309), - ), // amber-700 - const SizedBox(width: 8), - Text( - t.staff_shifts.status.swap_requested, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xFFB45309), - ), - ), - ], - ), - ) - else - Column( - children: [ - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - onPressed: widget.onAccept, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Text( - status == 'pending' - ? t.staff_shifts.action.confirm - : "Book Shift", - style: const TextStyle( - fontWeight: FontWeight.w600, - ), - ), - ), - ), - if (status == 'pending' || - status == 'open') ...[ - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton( - onPressed: widget.onDecline, - style: OutlinedButton.styleFrom( - foregroundColor: - UiColors.destructive, - side: const BorderSide( - color: UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Text( - t.staff_shifts.action.decline), - ), - ), - ], - ], - ), - ], - ), - ), - ], - ) - : const SizedBox.shrink(), - ), ], ), ), ); } - - Widget _buildStatCard(IconData icon, String value, String label) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: UiColors.border), - ), - child: Column( - children: [ - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: UiColors.iconSecondary), - ), - const SizedBox(height: 8), - Text( - value, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - label, - style: UiTypography.footnote2r.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - ); - } - - Widget _buildTimeBox(String label, String time) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Text( - label, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 4), - Text( - _formatTime(time), - style: UiTypography.display2m.copyWith( - fontSize: 20, - color: UiColors.textPrimary, - ), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 215da086..6524b050 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../../blocs/shifts/shifts_bloc.dart'; +import '../../navigation/shifts_navigator.dart'; import '../../styles/shifts_styles.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -23,74 +23,6 @@ class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; - void _bookShift(String id) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Book Shift'), - content: const Text( - 'Do you want to instantly book this shift?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().add(BookShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Booking processed!'), - backgroundColor: Color(0xFF10B981), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF10B981), - ), - child: const Text('Book'), - ), - ], - ), - ); - } - - void _declineShift(String id) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Decline Shift'), - content: const Text( - 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().add(DeclineShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: const Color(0xFFEF4444), - ), - child: const Text('Decline'), - ), - ], - ), - ); - } - Widget _buildFilterTab(String id, String label) { final isSelected = _jobType == id; return GestureDetector( @@ -241,8 +173,6 @@ class _FindShiftsTabState extends State { padding: const EdgeInsets.only(bottom: 12), child: MyShiftCard( shift: shift, - onAccept: () => _bookShift(shift.id), - onDecline: () => _declineShift(shift.id), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart index 5edb6eff..b89783ba 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import '../../navigation/shifts_navigator.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -30,9 +32,11 @@ class HistoryShiftsTab extends StatelessWidget { ...historyShifts.map( (shift) => Padding( padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard( - shift: shift, - historyMode: true, + child: GestureDetector( + onTap: () => Modular.to.pushShiftDetails(shift), + child: MyShiftCard( + shift: shift, + ), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart new file mode 100644 index 00000000..78fddf80 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart @@ -0,0 +1,31 @@ +import 'package:flutter_modular/flutter_modular.dart'; +import 'domain/repositories/shifts_repository_interface.dart'; +import 'data/repositories_impl/shifts_repository_impl.dart'; +import 'domain/usecases/get_shift_details_usecase.dart'; +import 'domain/usecases/accept_shift_usecase.dart'; +import 'domain/usecases/decline_shift_usecase.dart'; +import 'domain/usecases/apply_for_shift_usecase.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; +import 'presentation/pages/shift_details_page.dart'; + +class ShiftDetailsModule extends Module { + @override + void binds(Injector i) { + // Repository + i.add(ShiftsRepositoryImpl.new); + + // UseCases + i.add(GetShiftDetailsUseCase.new); + i.add(AcceptShiftUseCase.new); + i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); + + // Bloc + i.add(ShiftDetailsBloc.new); + } + + @override + void routes(RouteManager r) { + r.child('/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart index bb8e9f9a..a1adddc4 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -11,6 +11,7 @@ import 'domain/usecases/decline_shift_usecase.dart'; import 'domain/usecases/apply_for_shift_usecase.dart'; import 'domain/usecases/get_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; import 'presentation/pages/shifts_page.dart'; import 'presentation/pages/shift_details_page.dart'; @@ -33,11 +34,11 @@ class StaffShiftsModule extends Module { // Bloc i.add(ShiftsBloc.new); + i.add(ShiftDetailsBloc.new); } @override void routes(RouteManager r) { r.child('/', child: (_) => const ShiftsPage()); - r.child('/details/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart index 28ae0ac4..7d0a0518 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart @@ -1,4 +1,6 @@ library staff_shifts; export 'src/staff_shifts_module.dart'; +export 'src/shift_details_module.dart'; +export 'src/presentation/navigation/shifts_navigator.dart'; diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index d7f5e3e0..661aa05d 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -77,6 +77,10 @@ class StaffMainModule extends Module { '/availability', module: StaffAvailabilityModule(), ); + r.module( + '/shift-details', + module: ShiftDetailsModule(), + ); } } From 0b38383aabdb2185cadb039ec41845431ebd8499 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 20:46:34 -0500 Subject: [PATCH 05/13] feat: Add DateTimeUtils for converting UTC to device local time and update imports --- apps/mobile/packages/core/lib/core.dart | 1 + .../core/lib/src/utils/date_time_utils.dart | 7 ++++++ .../shifts_repository_impl.dart | 25 ++++++++++++++----- .../widgets/tabs/find_shifts_tab.dart | 5 ++-- 4 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 apps/mobile/packages/core/lib/src/utils/date_time_utils.dart diff --git a/apps/mobile/packages/core/lib/core.dart b/apps/mobile/packages/core/lib/core.dart index f46af624..c6536ad4 100644 --- a/apps/mobile/packages/core/lib/core.dart +++ b/apps/mobile/packages/core/lib/core.dart @@ -2,3 +2,4 @@ library core; export 'src/domain/arguments/usecase_argument.dart'; export 'src/domain/usecases/usecase.dart'; +export 'src/utils/date_time_utils.dart'; diff --git a/apps/mobile/packages/core/lib/src/utils/date_time_utils.dart b/apps/mobile/packages/core/lib/src/utils/date_time_utils.dart new file mode 100644 index 00000000..1d142b33 --- /dev/null +++ b/apps/mobile/packages/core/lib/src/utils/date_time_utils.dart @@ -0,0 +1,7 @@ + +class DateTimeUtils { + /// Converts a [DateTime] (assumed UTC if not specified) to the device's local time. + static DateTime toDeviceTime(DateTime date) { + return date.toLocal(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 6f76b2a7..564d2848 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -4,6 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; import 'package:intl/intl.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_core/core.dart'; import '../../domain/repositories/shifts_repository_interface.dart'; class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { @@ -51,15 +52,27 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { DateTime? _toDateTime(dynamic t) { if (t == null) return null; - try { - return DateTime.tryParse(t.toJson() as String); - } catch (_) { + DateTime? dt; + if (t is Timestamp) { + dt = t.toDateTime(); + } else if (t is String) { + dt = DateTime.tryParse(t); + } else { try { - return DateTime.tryParse(t.toString()); - } catch (e) { - return null; + dt = DateTime.tryParse(t.toJson() as String); + } catch (_) { + try { + dt = DateTime.tryParse(t.toString()); + } catch (e) { + dt = null; + } } } + + if (dt != null) { + return DateTimeUtils.toDeviceTime(dt); + } + return null; } @override diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 6524b050..648e9a85 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../../navigation/shifts_navigator.dart'; + import '../../styles/shifts_styles.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; From 144976de00522c70ad4cd314c046210521270b14 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 21:04:08 -0500 Subject: [PATCH 06/13] feat: Update roleId assignment and filter past shifts in ShiftsBloc --- .../shifts_repository_impl.dart | 2 +- .../presentation/blocs/shifts/shifts_bloc.dart | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 564d2848..3d40e268 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -246,7 +246,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { await _dataConnect.createApplication( shiftId: shiftId, staffId: staffId, - roleId: role.id, + roleId: role.roleId, status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING, origin: dc.ApplicationOrigin.STAFF, ).execute(); diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart index 1b21f68b..d2983315 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -55,7 +55,7 @@ class ShiftsBloc extends Bloc { myShifts: myShiftsResult, pendingShifts: pendingResult, cancelledShifts: cancelledResult, - availableShifts: availableResult, + availableShifts: _filterPastShifts(availableResult), historyShifts: historyResult, searchQuery: '', jobType: 'all', @@ -81,7 +81,7 @@ class ShiftsBloc extends Bloc { )); emit(currentState.copyWith( - availableShifts: result, + availableShifts: _filterPastShifts(result), searchQuery: event.query ?? currentState.searchQuery, jobType: event.jobType ?? currentState.jobType, )); @@ -90,4 +90,17 @@ class ShiftsBloc extends Bloc { } } } + + List _filterPastShifts(List shifts) { + final now = DateTime.now(); + return shifts.where((shift) { + if (shift.date.isEmpty) return false; + try { + final shiftDate = DateTime.parse(shift.date); + return shiftDate.isAfter(now); + } catch (_) { + return false; + } + }).toList(); + } } From 820f475c515df83b85c256a5bb76a19128a8c8c9 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 21:48:42 -0500 Subject: [PATCH 07/13] feat: Add required and filled slots to Shift entity and update ShiftDetailsPage for capacity display --- .../domain/lib/src/entities/shifts/shift.dart | 6 + .../shifts_repository_impl.dart | 20 + .../pages/shift_details_page.dart | 530 +++++++++++------- 3 files changed, 349 insertions(+), 207 deletions(-) diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart index 4998c45b..2c6081ed 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -24,6 +24,8 @@ class Shift extends Equatable { final double? longitude; final String? status; final int? durationDays; // For multi-day shifts + final int? requiredSlots; + final int? filledSlots; const Shift({ required this.id, @@ -49,6 +51,8 @@ class Shift extends Equatable { this.longitude, this.status, this.durationDays, + this.requiredSlots, + this.filledSlots, }); @override @@ -76,6 +80,8 @@ class Shift extends Equatable { longitude, status, durationDays, + requiredSlots, + filledSlots, ]; } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 3d40e268..1b604078 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -127,6 +127,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: _mapStatus(status), description: shift.description, durationDays: shift.durationDays, + requiredSlots: shift.requiredSlots, + filledSlots: shift.filledSlots, )); } } @@ -182,6 +184,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: s.status?.stringValue.toLowerCase() ?? 'open', description: s.description, durationDays: s.durationDays, + requiredSlots: null, // Basic list doesn't fetch detailed role stats yet + filledSlots: null, )); } @@ -210,6 +214,20 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { final s = result.data.shift; if (s == null) return null; + int? required; + int? filled; + try { + final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); + if (rolesRes.data.shiftRoles.isNotEmpty) { + required = 0; + filled = 0; + for(var r in rolesRes.data.shiftRoles) { + required = (required ?? 0) + r.count; + filled = (filled ?? 0) + (r.assigned ?? 0); + } + } + } catch (_) {} + final startDt = _toDateTime(s.startTime); final endDt = _toDateTime(s.endTime); final createdDt = _toDateTime(s.createdAt); @@ -229,6 +247,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: s.status?.stringValue ?? 'OPEN', description: s.description, durationDays: s.durationDays, + requiredSlots: required, + filledSlots: filled, ); } catch (e) { return null; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 7ade6d31..14889f20 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -3,22 +3,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic -import 'package:intl/intl.dart'; +import 'package:intl/intl.dart'; import '../blocs/shift_details/shift_details_bloc.dart'; import '../blocs/shift_details/shift_details_event.dart'; import '../blocs/shift_details/shift_details_state.dart'; -import '../styles/shifts_styles.dart'; -import '../widgets/my_shift_card.dart'; class ShiftDetailsPage extends StatelessWidget { final String shiftId; final Shift? shift; - const ShiftDetailsPage({ - super.key, - required this.shiftId, - this.shift, - }); + const ShiftDetailsPage({super.key, required this.shiftId, this.shift}); String _formatTime(String time) { if (time.isEmpty) return ''; @@ -33,6 +27,16 @@ class ShiftDetailsPage extends StatelessWidget { } } + String _formatDate(String dateStr) { + if (dateStr.isEmpty) return ''; + try { + final date = DateTime.parse(dateStr); + return DateFormat('EEEE, MMMM d, y').format(date); + } catch (e) { + return dateStr; + } + } + double _calculateDuration(Shift shift) { if (shift.startTime.isEmpty || shift.endTime.isEmpty) { return 0; @@ -70,9 +74,7 @@ class ShiftDetailsPage extends StatelessWidget { const SizedBox(height: 8), Text( value, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), + style: UiTypography.title1m.copyWith(color: UiColors.textPrimary), ), Text( label, @@ -118,9 +120,9 @@ class ShiftDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => Modular.get() - ..add(LoadShiftDetailsEvent(shiftId)), + return BlocProvider( + create: (_) => + Modular.get()..add(LoadShiftDetailsEvent(shiftId)), child: BlocListener( listener: (context, state) { if (state is ShiftActionSuccess) { @@ -163,213 +165,322 @@ class ShiftDetailsPage extends StatelessWidget { final duration = _calculateDuration(displayShift); final estimatedTotal = (displayShift.hourlyRate) * duration; + final openSlots = + (displayShift.requiredSlots ?? 0) - + (displayShift.filledSlots ?? 0); return Scaffold( - backgroundColor: AppColors.krowBackground, - appBar: AppBar( - title: const Text("Shift Details"), - backgroundColor: Colors.white, - foregroundColor: AppColors.krowCharcoal, - elevation: 0.5, + appBar: UiAppBar( + title: displayShift.title, + showBackButton: true, + centerTitle: false, ), - body: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - MyShiftCard( - shift: displayShift, - // No direct actions on the card, handled by page buttons - ), - const SizedBox(height: 24), - - // Stats Row - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", - ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Vendor Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "VENDOR", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${displayShift.hourlyRate.toInt()}", - "Hourly Rate", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration.toInt()}", - "Hours", - ), - ), - ], - ), - const SizedBox(height: 24), - - // In/Out Time - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - displayShift.startTime, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - displayShift.endTime, - ), - ), - ], - ), - const SizedBox(height: 24), - - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "LOCATION", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - displayShift.location.isEmpty - ? "TBD" - : displayShift.location, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(displayShift!.locationAddress), - duration: const Duration(seconds: 3), + ), + const SizedBox(height: 8), + Row( + children: [ + Container( + width: 24, + height: 24, + child: displayShift.logoUrl != null + ? ClipRRect( + borderRadius: BorderRadius.circular( + 6, + ), + child: Image.network( + displayShift.logoUrl!, + fit: BoxFit.cover, + ), + ) + : const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 20, + ), ), - ); - }, - icon: const Icon(UiIcons.navigation, size: 14), - label: const Text( - "Get direction", - style: TextStyle(fontSize: 12), - ), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide(color: UiColors.border), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - height: 128, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), ), - child: const Center( - child: Icon( - UiIcons.mapPin, - color: UiColors.iconSecondary, - size: 32, + const SizedBox(width: 8), + Text( + displayShift.clientName, + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, ), ), - // Placeholder for Map - ), - ], - ), - const SizedBox(height: 24), + ], + ), + ], + ), + const SizedBox(height: 24), - // Additional Info - if (displayShift.description != null) ...[ - SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "ADDITIONAL INFO", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), + // Date Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "SHIFT DATE", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Text( + _formatDate(displayShift.date), + style: UiTypography.headline5m.copyWith( + color: UiColors.textPrimary, ), - const SizedBox(height: 8), - Text( - displayShift.description!, - style: UiTypography.body2m.copyWith( - color: UiColors.textPrimary, - ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // Worker Capacity / Open Slots + if ((displayShift.requiredSlots ?? 0) > 0) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF0FDF4), // green-50 + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFBBF7D0), + ), // green-200 + ), + child: Row( + children: [ + const Icon( + Icons.people_alt_outlined, + size: 20, + color: Color(0xFF15803D), + ), // green-700, using Material Icon as generic fallback + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "$openSlots spots remaining", + style: UiTypography.body2b.copyWith( + color: const Color(0xFF15803D), + ), + ), + Text( + "${displayShift.filledSlots ?? 0} filled out of ${displayShift.requiredSlots}", + style: UiTypography.body3r.copyWith( + color: const Color(0xFF166534), + ), + ), + ], ), - ], + ), + SizedBox( + width: 60, + child: LinearProgressIndicator( + value: (displayShift.requiredSlots! > 0) + ? (displayShift.filledSlots ?? 0) / + displayShift.requiredSlots! + : 0, + backgroundColor: Colors.white, + color: const Color(0xFF15803D), + minHeight: 6, + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Stats Grid + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + crossAxisSpacing: 12, + childAspectRatio: 0.85, + children: [ + _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total Pay", + ), + _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toInt()}", + "Per Hour", + ), + _buildStatCard( + UiIcons.clock, + "${duration.toInt()}h", + "Duration", + ), + ], + ), + const SizedBox(height: 24), + + // Shift Timing + Row( + children: [ + Expanded( + child: _buildTimeBox( + "START TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildTimeBox( + "END TIME", + displayShift.endTime, ), ), ], + ), + const SizedBox(height: 24), + + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "LOCATION", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.locationAddress, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // Additional Info + if (displayShift.description != null) ...[ + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ADDITIONAL INFO", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + displayShift.description!, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ), ], - ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => + _declineShift(context, displayShift!.id), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + side: const BorderSide( + color: Color(0xFFEF4444), + ), + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Decline"), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => + _bookShift(context, displayShift!.id), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + child: const Text("Book Shift"), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, + ), + ], ), ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => _declineShift(context, displayShift!.id), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFFEF4444), - side: const BorderSide(color: Color(0xFFEF4444)), - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text("Decline"), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: () => _bookShift(context, displayShift!.id), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF10B981), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text("Book Shift"), - ), - ), - ], - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 10), - ], - ), + ), + ], ), ); }, @@ -392,7 +503,9 @@ class ShiftDetailsPage extends StatelessWidget { TextButton( onPressed: () { Navigator.of(ctx).pop(); - BlocProvider.of(context).add(BookShiftDetailsEvent(id)); + BlocProvider.of( + context, + ).add(BookShiftDetailsEvent(id)); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFF10B981), @@ -410,7 +523,8 @@ class ShiftDetailsPage extends StatelessWidget { builder: (ctx) => AlertDialog( title: const Text('Decline Shift'), content: const Text( - 'Are you sure you want to decline this shift? It will be hidden from your available jobs.'), + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', + ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), @@ -419,7 +533,9 @@ class ShiftDetailsPage extends StatelessWidget { TextButton( onPressed: () { Navigator.of(ctx).pop(); - BlocProvider.of(context).add(DeclineShiftDetailsEvent(id)); + BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(id)); }, style: TextButton.styleFrom( foregroundColor: const Color(0xFFEF4444), From 4b5a3d13045d107e126215fd6cf84f1d748eaf50 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 02:06:28 -0500 Subject: [PATCH 08/13] feat: Refactor imports and enhance navigation for shift details in staff home feature --- .../auth_repository_impl.dart | 2 +- .../repositories/home_repository_impl.dart | 2 +- .../domain/repositories/home_repository.dart | 2 +- .../src/domain/usecases/get_home_shifts.dart | 2 +- .../src/presentation/blocs/home_cubit.dart | 2 +- .../navigation/home_navigator.dart | 6 ++++++ .../home_page/recommended_shift_card.dart | 19 +++++++------------ .../src/presentation/widgets/shift_card.dart | 2 +- .../packages/features/staff/home/pubspec.yaml | 2 ++ .../navigation/shifts_navigator.dart | 2 -- 10 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 43674a96..87146306 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -3,7 +3,6 @@ import 'dart:developer' as developer; import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' show InvalidCredentialsException, @@ -15,6 +14,7 @@ import 'package:krow_domain/krow_domain.dart' UnauthorizedAppException, PasswordMismatchException, GoogleOnlyAccountException; +import 'package:krow_domain/krow_domain.dart' as domain; import '../../domain/repositories/auth_repository_interface.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 51247a7d..6a4ecc62 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -1,7 +1,7 @@ import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; extension TimestampExt on Timestamp { diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart index a280f4cf..320061d6 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart @@ -1,4 +1,4 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Repository interface for home screen data operations. /// diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart index 2e9ed17c..dd8d7958 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart @@ -1,4 +1,4 @@ -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; /// Use case for fetching all shifts displayed on the home screen. diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart index 27ffb317..85a16eca 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; import 'package:staff_home/src/domain/usecases/get_home_shifts.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart index 056a5636..4e8dfc0e 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart @@ -1,4 +1,5 @@ import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; /// Extension on [IModularNavigator] providing typed navigation helpers /// for the Staff Home feature (worker home screen). @@ -40,4 +41,9 @@ extension HomeNavigator on IModularNavigator { void pushSettings() { pushNamed('/settings'); } + + /// Navigates to the shift details page for the given [shift]. + void pushShiftDetails(Shift shift) { + pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift); + } } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart index 7940ff30..3a4ef59d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; - -import 'package:design_system/design_system.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:staff_home/src/presentation/navigation/home_navigator.dart'; class RecommendedShiftCard extends StatelessWidget { final Shift shift; @@ -18,13 +19,7 @@ class RecommendedShiftCard extends StatelessWidget { return GestureDetector( onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(recI18n.applied_for(title: shift.title)), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), - ); + Modular.to.pushShiftDetails(shift); }, child: Container( width: 300, diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart index 3990fe9c..f2a95f0d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart @@ -4,7 +4,7 @@ import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:design_system/design_system.dart'; -import 'package:staff_home/src/domain/entities/shift.dart'; +import 'package:krow_domain/krow_domain.dart'; class ShiftCard extends StatefulWidget { final Shift shift; diff --git a/apps/mobile/packages/features/staff/home/pubspec.yaml b/apps/mobile/packages/features/staff/home/pubspec.yaml index e35bd26d..8d6afcfd 100644 --- a/apps/mobile/packages/features/staff/home/pubspec.yaml +++ b/apps/mobile/packages/features/staff/home/pubspec.yaml @@ -28,6 +28,8 @@ dependencies: path: ../../../core krow_domain: path: ../../../domain + staff_shifts: + path: ../shifts krow_data_connect: path: ../../../data_connect firebase_data_connect: diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart index 306caa5a..007b4e00 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart @@ -5,6 +5,4 @@ extension ShiftsNavigator on IModularNavigator { void pushShiftDetails(Shift shift) { pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift); } - - // Example for going back or internal navigation if needed } From 3ecc89fcb24a0f92bfb7d046ac2908d88ae8de66 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 02:24:39 -0500 Subject: [PATCH 09/13] feat: Update Timestamp extension to use DateTimeUtils for improved date conversion --- .../home/lib/src/data/repositories/home_repository_impl.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 6a4ecc62..4798952c 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -2,11 +2,12 @@ import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:krow_core/core.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart'; extension TimestampExt on Timestamp { DateTime toDate() { - return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000); + return DateTimeUtils.toDeviceTime(toDateTime()); } } From 3ed1add2cc356beabda5713299f6f4ff154846d7 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 02:38:28 -0500 Subject: [PATCH 10/13] feat: Integrate staff name retrieval and display in home header --- .../repositories/home_repository_impl.dart | 7 +- .../domain/repositories/home_repository.dart | 3 + .../src/presentation/blocs/home_cubit.dart | 4 + .../src/presentation/blocs/home_state.dart | 23 +++--- .../presentation/pages/worker_home_page.dart | 7 +- .../widgets/home_page/home_header.dart | 77 ++++++++++--------- 6 files changed, 74 insertions(+), 47 deletions(-) diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 4798952c..d2d61c63 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -11,7 +11,6 @@ extension TimestampExt on Timestamp { } } - class HomeRepositoryImpl implements HomeRepository { HomeRepositoryImpl(); @@ -73,6 +72,12 @@ class HomeRepositoryImpl implements HomeRepository { } } + @override + Future getStaffName() async { + final session = StaffSessionStore.instance.session; + return session?.staff?.name; + } + // Mappers specific to Home's Domain Entity 'Shift' // Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift. diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart index 320061d6..df35f9d2 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart @@ -14,4 +14,7 @@ abstract class HomeRepository { /// Retrieves shifts recommended for the worker based on their profile. Future> getRecommendedShifts(); + + /// Retrieves the current staff member's name. + Future getStaffName(); } diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart index 85a16eca..792a32eb 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart @@ -10,9 +10,11 @@ part 'home_state.dart'; /// Simple Cubit to manage home page state (shifts + loading/error). class HomeCubit extends Cubit { final GetHomeShifts _getHomeShifts; + final HomeRepository _repository; HomeCubit(HomeRepository repository) : _getHomeShifts = GetHomeShifts(repository), + _repository = repository, super(const HomeState.initial()); Future loadShifts() async { @@ -20,6 +22,7 @@ class HomeCubit extends Cubit { emit(state.copyWith(status: HomeStatus.loading)); try { final result = await _getHomeShifts.call(); + final name = await _repository.getStaffName(); if (isClosed) return; emit( state.copyWith( @@ -27,6 +30,7 @@ class HomeCubit extends Cubit { todayShifts: result.today, tomorrowShifts: result.tomorrow, recommendedShifts: result.recommended, + staffName: name, // Mock profile status for now, ideally fetched from a user repository isProfileComplete: false, ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart index e67f454b..0713d7a1 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart @@ -9,6 +9,7 @@ class HomeState extends Equatable { final List recommendedShifts; final bool autoMatchEnabled; final bool isProfileComplete; + final String? staffName; final String? errorMessage; const HomeState({ @@ -18,6 +19,7 @@ class HomeState extends Equatable { this.recommendedShifts = const [], this.autoMatchEnabled = false, this.isProfileComplete = false, + this.staffName, this.errorMessage, }); @@ -30,6 +32,7 @@ class HomeState extends Equatable { List? recommendedShifts, bool? autoMatchEnabled, bool? isProfileComplete, + String? staffName, String? errorMessage, }) { return HomeState( @@ -39,18 +42,20 @@ class HomeState extends Equatable { recommendedShifts: recommendedShifts ?? this.recommendedShifts, autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled, isProfileComplete: isProfileComplete ?? this.isProfileComplete, + staffName: staffName ?? this.staffName, errorMessage: errorMessage ?? this.errorMessage, ); } @override List get props => [ - status, - todayShifts, - tomorrowShifts, - recommendedShifts, - autoMatchEnabled, - isProfileComplete, - errorMessage, - ]; -} + status, + todayShifts, + tomorrowShifts, + recommendedShifts, + autoMatchEnabled, + isProfileComplete, + staffName, + errorMessage, + ]; +} \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index 61ff3d9e..1cbb51fc 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -48,7 +48,12 @@ class WorkerHomePage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const HomeHeader(), + BlocBuilder( + buildWhen: (previous, current) => previous.staffName != current.staffName, + builder: (context, state) { + return HomeHeader(userName: state.staffName); + }, + ), Padding( padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4), child: Column( diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart index ea85d499..247d380e 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart @@ -2,15 +2,21 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; - /// Header widget for the staff home page, using design system tokens. class HomeHeader extends StatelessWidget { + final String? userName; + /// Creates a [HomeHeader]. - const HomeHeader({super.key}); + const HomeHeader({super.key, this.userName}); @override Widget build(BuildContext context) { final headerI18n = t.staff.home.header; + final nameToDisplay = userName ?? headerI18n.user_name_placeholder; + final initial = nameToDisplay.isNotEmpty + ? nameToDisplay[0].toUpperCase() + : 'K'; + return Padding( padding: EdgeInsets.fromLTRB( UiConstants.space4, @@ -18,45 +24,44 @@ class HomeHeader extends StatelessWidget { UiConstants.space4, UiConstants.space3, ), - child:Row( + child: Row( + spacing: UiConstants.space3, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: UiColors.primary.withOpacity(0.2), + width: 2, + ), + ), + child: CircleAvatar( + backgroundColor: UiColors.primary.withOpacity(0.1), + child: Text( + initial, + style: const TextStyle( + color: UiColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: UiColors.primary.withOpacity(0.2), - width: 2, - ), - ), - child: CircleAvatar( - backgroundColor: UiColors.primary.withOpacity(0.1), - child: const Text( - 'K', - style: TextStyle( - color: UiColors.primary, - fontWeight: FontWeight.bold, - ), - ), + Text( + headerI18n.welcome_back, + style: UiTypography.body3r.copyWith( + color: UiColors.mutedForeground, ), ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - headerI18n.welcome_back, - style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground), - ), - Text( - headerI18n.user_name_placeholder, - style: UiTypography.headline4m, - ), - ], - ), + Text(nameToDisplay, style: UiTypography.headline4m), ], ), + ], + ), ); } } From 56dbf7728ffce7db177ad02fce3f31c49b7f395b Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 02:46:30 -0500 Subject: [PATCH 11/13] feat: Enhance shift filtering logic and update header text style for improved UI --- .../src/data/repositories/home_repository_impl.dart | 10 +++++++++- .../presentation/widgets/home_page/home_header.dart | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index d2d61c63..508e350a 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -63,7 +63,15 @@ class HomeRepositoryImpl implements HomeRepository { final response = await ExampleConnector.instance.listShifts().execute(); return response.data.shifts - .where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN) + .where((s) { + final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN; + if (!isOpen) return false; + + final start = s.startTime?.toDate(); + if (start == null) return false; + + return start.isAfter(DateTime.now()); + }) .take(10) .map((s) => _mapConnectorShiftToDomain(s)) .toList(); diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart index 247d380e..17127ce5 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_header.dart @@ -53,9 +53,7 @@ class HomeHeader extends StatelessWidget { children: [ Text( headerI18n.welcome_back, - style: UiTypography.body3r.copyWith( - color: UiColors.mutedForeground, - ), + style: UiTypography.body3r.textSecondary, ), Text(nameToDisplay, style: UiTypography.headline4m), ], From c95e4ed9c734fe4419c7360a1f6b9e96bbe5f4b0 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 03:38:42 -0500 Subject: [PATCH 12/13] feat: Update package names and application IDs to use com.krowwithus for client and staff apps --- apps/mobile/apps/client/android/app/build.gradle.kts | 2 +- .../main/kotlin/com/example/krow_client/MainActivity.kt | 2 +- apps/mobile/apps/client/linux/CMakeLists.txt | 2 +- apps/mobile/apps/staff/android/app/build.gradle.kts | 2 +- apps/mobile/apps/staff/android/app/google-services.json | 8 ++++++++ .../main/kotlin/com/example/krow_staff/MainActivity.kt | 2 +- apps/mobile/apps/staff/linux/CMakeLists.txt | 2 +- 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 04b25374..202bc20b 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.example.krow_client" + namespace = "com.krowwithus.client" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion diff --git a/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt b/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt index 419b3bd4..3e393b5d 100644 --- a/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt +++ b/apps/mobile/apps/client/android/app/src/main/kotlin/com/example/krow_client/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.krow_client +package com.krowwithus.client import io.flutter.embedding.android.FlutterActivity diff --git a/apps/mobile/apps/client/linux/CMakeLists.txt b/apps/mobile/apps/client/linux/CMakeLists.txt index 6f1df0fe..350d88d7 100644 --- a/apps/mobile/apps/client/linux/CMakeLists.txt +++ b/apps/mobile/apps/client/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "krow_client") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.krow_client") +set(APPLICATION_ID "com.krowwithus.client") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 80f2b222..8764d57b 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.example.krow_staff" + namespace = "com.krowwithus.staff" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion diff --git a/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/google-services.json index 13b4592b..42bb1f02 100644 --- a/apps/mobile/apps/staff/android/app/google-services.json +++ b/apps/mobile/apps/staff/android/app/google-services.json @@ -193,6 +193,14 @@ } }, "oauth_client": [ + { + "client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.krowwithus.staff", + "certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" + } + }, { "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", "client_type": 3 diff --git a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt index 13520833..b892977d 100644 --- a/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt +++ b/apps/mobile/apps/staff/android/app/src/main/kotlin/com/example/krow_staff/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.krow_staff +package com.krowwithus.staff import io.flutter.embedding.android.FlutterActivity diff --git a/apps/mobile/apps/staff/linux/CMakeLists.txt b/apps/mobile/apps/staff/linux/CMakeLists.txt index b222a83e..56ce18bd 100644 --- a/apps/mobile/apps/staff/linux/CMakeLists.txt +++ b/apps/mobile/apps/staff/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "krow_staff") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.krow_staff") +set(APPLICATION_ID "com.krowwithus.staff") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. From 1625f81ce6c022aa97889ba3ded689a4b50fb792 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 1 Feb 2026 03:47:39 -0500 Subject: [PATCH 13/13] feat: Integrate profile setup use case and repository for profile submission --- .../profile_setup_repository_impl.dart | 67 +++++++++++++++++++ .../profile_setup_repository.dart | 12 ++++ .../submit_profile_setup_usecase.dart | 25 +++++++ .../profile_setup/profile_setup_bloc.dart | 58 +++------------- .../lib/staff_authentication.dart | 13 +++- 5 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart create mode 100644 apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart create mode 100644 apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart new file mode 100644 index 00000000..0903fa83 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/profile_setup_repository_impl.dart @@ -0,0 +1,67 @@ +import 'package:firebase_auth/firebase_auth.dart' as auth; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/profile_setup_repository.dart'; + +class ProfileSetupRepositoryImpl implements ProfileSetupRepository { + final auth.FirebaseAuth _firebaseAuth; + final ExampleConnector _dataConnect; + + ProfileSetupRepositoryImpl({ + required auth.FirebaseAuth firebaseAuth, + required ExampleConnector dataConnect, + }) : _firebaseAuth = firebaseAuth, + _dataConnect = dataConnect; + + @override + Future submitProfile({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }) async { + final auth.User? firebaseUser = _firebaseAuth.currentUser; + if (firebaseUser == null) { + throw Exception('User not authenticated.'); + } + + final StaffSession? session = StaffSessionStore.instance.session; + final String email = session?.user.email ?? ''; + final String? phone = firebaseUser.phoneNumber; + + final fdc.OperationResult + result = await _dataConnect + .createStaff( + userId: firebaseUser.uid, + fullName: fullName, + ) + .bio(bio) + .preferredLocations(preferredLocations) + .maxDistanceMiles(maxDistanceMiles.toInt()) + .industries(industries) + .skills(skills) + .email(email.isEmpty ? null : email) + .phone(phone) + .execute(); + + final String staffId = result.data.staff_insert.id; + + final Staff staff = Staff( + id: staffId, + authProviderId: firebaseUser.uid, + name: fullName, + email: email, + phone: phone, + status: StaffStatus.completedProfile, + ); + + if (session != null) { + StaffSessionStore.instance.setSession( + StaffSession(user: session.user, staff: staff), + ); + } + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart new file mode 100644 index 00000000..8b99f0f9 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/profile_setup_repository.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; + +abstract class ProfileSetupRepository { + Future submitProfile({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }); +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart b/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart new file mode 100644 index 00000000..b69f5fe6 --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/domain/usecases/submit_profile_setup_usecase.dart @@ -0,0 +1,25 @@ +import '../repositories/profile_setup_repository.dart'; + +class SubmitProfileSetup { + final ProfileSetupRepository repository; + + SubmitProfileSetup(this.repository); + + Future call({ + required String fullName, + String? bio, + required List preferredLocations, + required double maxDistanceMiles, + required List industries, + required List skills, + }) { + return repository.submitProfile( + fullName: fullName, + bio: bio, + preferredLocations: preferredLocations, + maxDistanceMiles: maxDistanceMiles, + industries: industries, + skills: skills, + ); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart index 93d8b44f..324ea906 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/blocs/profile_setup/profile_setup_bloc.dart @@ -1,8 +1,5 @@ -import 'package:firebase_auth/firebase_auth.dart' as auth; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:krow_domain/krow_domain.dart'; +import '../../../domain/usecases/submit_profile_setup_usecase.dart'; import 'profile_setup_event.dart'; import 'profile_setup_state.dart'; @@ -13,10 +10,8 @@ export 'profile_setup_state.dart'; /// BLoC responsible for managing the profile setup state and logic. class ProfileSetupBloc extends Bloc { ProfileSetupBloc({ - required auth.FirebaseAuth firebaseAuth, - required dc.ExampleConnector dataConnect, - }) : _firebaseAuth = firebaseAuth, - _dataConnect = dataConnect, + required SubmitProfileSetup submitProfileSetup, + }) : _submitProfileSetup = submitProfileSetup, super(const ProfileSetupState()) { on(_onFullNameChanged); on(_onBioChanged); @@ -27,8 +22,7 @@ class ProfileSetupBloc extends Bloc { on(_onSubmitted); } - final auth.FirebaseAuth _firebaseAuth; - final dc.ExampleConnector _dataConnect; + final SubmitProfileSetup _submitProfileSetup; /// Handles the [ProfileSetupFullNameChanged] event. void _onFullNameChanged( @@ -86,44 +80,14 @@ class ProfileSetupBloc extends Bloc { emit(state.copyWith(status: ProfileSetupStatus.loading)); try { - final auth.User? firebaseUser = _firebaseAuth.currentUser; - if (firebaseUser == null) { - throw Exception('User not authenticated.'); - } - - final dc.StaffSession? session = dc.StaffSessionStore.instance.session; - final String email = session?.user.email ?? ''; - final String? phone = firebaseUser.phoneNumber; - - final fdc.OperationResult - result = await _dataConnect - .createStaff( - userId: firebaseUser.uid, - fullName: state.fullName, - ) - .bio(state.bio.isEmpty ? null : state.bio) - .preferredLocations(state.preferredLocations) - .maxDistanceMiles(state.maxDistanceMiles.toInt()) - .industries(state.industries) - .skills(state.skills) - .email(email.isEmpty ? null : email) - .phone(phone) - .execute(); - - final String staffId = result.data.staff_insert.id ; - final Staff staff = Staff( - id: staffId, - authProviderId: firebaseUser.uid, - name: state.fullName, - email: email, - phone: phone, - status: StaffStatus.completedProfile, + await _submitProfileSetup( + fullName: state.fullName, + bio: state.bio.isEmpty ? null : state.bio, + preferredLocations: state.preferredLocations, + maxDistanceMiles: state.maxDistanceMiles, + industries: state.industries, + skills: state.skills, ); - if (session != null) { - dc.StaffSessionStore.instance.setSession( - dc.StaffSession(user: session.user, staff: staff), - ); - } emit(state.copyWith(status: ProfileSetupStatus.success)); } catch (e) { diff --git a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart index 19421456..b98c5356 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart @@ -8,6 +8,9 @@ import 'package:staff_authentication/src/data/repositories_impl/auth_repository_ import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart'; import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart'; import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart'; +import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart'; +import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart'; +import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart'; import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart'; import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart'; import 'package:staff_authentication/src/presentation/pages/get_started_page.dart'; @@ -35,10 +38,17 @@ class StaffAuthenticationModule extends Module { dataConnect: ExampleConnector.instance, ), ); + i.addLazySingleton( + () => ProfileSetupRepositoryImpl( + firebaseAuth: firebase.FirebaseAuth.instance, + dataConnect: ExampleConnector.instance, + ), + ); // UseCases i.addLazySingleton(SignInWithPhoneUseCase.new); i.addLazySingleton(VerifyOtpUseCase.new); + i.addLazySingleton(SubmitProfileSetup.new); // BLoCs i.addLazySingleton( @@ -49,8 +59,7 @@ class StaffAuthenticationModule extends Module { ); i.add( () => ProfileSetupBloc( - firebaseAuth: firebase.FirebaseAuth.instance, - dataConnect: ExampleConnector.instance, + submitProfileSetup: i.get(), ), ); }