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, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +}