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 e24d6477..f3fb278e 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -31,6 +31,9 @@ class Shift extends Equatable { final bool? hasApplied; final double? totalValue; final Break? breakInfo; + final String? orderId; + final String? orderType; + final List? schedules; const Shift({ required this.id, @@ -62,6 +65,9 @@ class Shift extends Equatable { this.hasApplied, this.totalValue, this.breakInfo, + this.orderId, + this.orderType, + this.schedules, }); @override @@ -95,9 +101,27 @@ class Shift extends Equatable { hasApplied, totalValue, breakInfo, + orderId, + orderType, + schedules, ]; } +class ShiftSchedule extends Equatable { + const ShiftSchedule({ + required this.date, + required this.startTime, + required this.endTime, + }); + + final String date; + final String startTime; + final String endTime; + + @override + List get props => [date, startTime, endTime]; +} + class ShiftManager extends Equatable { const ShiftManager({required this.name, required this.phone, this.avatar}); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart index 888bd150..c5041687 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart @@ -20,6 +20,42 @@ class PermanentOrderView extends StatelessWidget { /// Creates a [PermanentOrderView]. const PermanentOrderView({super.key}); + DateTime _firstPermanentShiftDate( + DateTime startDate, + List permanentDays, + ) { + final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); + final DateTime end = start.add(const Duration(days: 29)); + final Set selected = permanentDays.toSet(); + for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { + if (selected.contains(_weekdayLabel(day))) { + return day; + } + } + return start; + } + + String _weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: + return 'MON'; + case DateTime.tuesday: + return 'TUE'; + case DateTime.wednesday: + return 'WED'; + case DateTime.thursday: + return 'THU'; + case DateTime.friday: + return 'FRI'; + case DateTime.saturday: + return 'SAT'; + case DateTime.sunday: + return 'SUN'; + default: + return 'SUN'; + } + } + @override Widget build(BuildContext context) { final TranslationsClientCreateOrderPermanentEn labels = @@ -42,6 +78,10 @@ class PermanentOrderView extends StatelessWidget { }, builder: (BuildContext context, PermanentOrderState state) { if (state.status == PermanentOrderStatus.success) { + final DateTime initialDate = _firstPermanentShiftDate( + state.startDate, + state.permanentDays, + ); return PermanentOrderSuccessView( title: labels.title, message: labels.subtitle, @@ -50,7 +90,7 @@ class PermanentOrderView extends StatelessWidget { ClientPaths.orders, (_) => false, arguments: { - 'initialDate': state.startDate.toIso8601String(), + 'initialDate': initialDate.toIso8601String(), }, ), ); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart index 89a20519..a6f173c8 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart @@ -20,6 +20,43 @@ class RecurringOrderView extends StatelessWidget { /// Creates a [RecurringOrderView]. const RecurringOrderView({super.key}); + DateTime _firstRecurringShiftDate( + DateTime startDate, + DateTime endDate, + List recurringDays, + ) { + final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); + final DateTime end = DateTime(endDate.year, endDate.month, endDate.day); + final Set selected = recurringDays.toSet(); + for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { + if (selected.contains(_weekdayLabel(day))) { + return day; + } + } + return start; + } + + String _weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: + return 'MON'; + case DateTime.tuesday: + return 'TUE'; + case DateTime.wednesday: + return 'WED'; + case DateTime.thursday: + return 'THU'; + case DateTime.friday: + return 'FRI'; + case DateTime.saturday: + return 'SAT'; + case DateTime.sunday: + return 'SUN'; + default: + return 'SUN'; + } + } + @override Widget build(BuildContext context) { final TranslationsClientCreateOrderRecurringEn labels = @@ -44,6 +81,15 @@ class RecurringOrderView extends StatelessWidget { }, builder: (BuildContext context, RecurringOrderState state) { if (state.status == RecurringOrderStatus.success) { + final DateTime maxEndDate = + state.startDate.add(const Duration(days: 29)); + final DateTime effectiveEndDate = + state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate; + final DateTime initialDate = _firstRecurringShiftDate( + state.startDate, + effectiveEndDate, + state.recurringDays, + ); return RecurringOrderSuccessView( title: labels.title, message: labels.subtitle, @@ -52,7 +98,7 @@ class RecurringOrderView extends StatelessWidget { ClientPaths.orders, (_) => false, arguments: { - 'initialDate': state.startDate.toIso8601String(), + 'initialDate': initialDate.toIso8601String(), }, ), ); 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 8be4f612..53667bde 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 @@ -227,6 +227,8 @@ class ShiftsRepositoryImpl filledSlots: sr.assigned ?? 0, latitude: sr.shift.latitude, longitude: sr.shift.longitude, + orderId: sr.shift.order.id, + orderType: sr.shift.order.orderType?.stringValue, breakInfo: BreakAdapter.fromData( isPaid: sr.isBreakPaid ?? false, breakTime: sr.breakType?.stringValue, 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 86352524..f34b405b 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 @@ -77,6 +77,13 @@ class _MyShiftCardState extends State { String _getShiftType() { // Handling potential localization key availability try { + final String orderType = (widget.shift.orderType ?? '').toUpperCase(); + if (orderType == 'PERMANENT') { + return t.staff_shifts.filter.long_term; + } + if (orderType == 'RECURRING') { + return t.staff_shifts.filter.multi_day; + } if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { return t.staff_shifts.filter.long_term; } @@ -133,6 +140,24 @@ class _MyShiftCardState extends State { statusText = status?.toUpperCase() ?? ""; } + final schedules = widget.shift.schedules ?? []; + final hasSchedules = schedules.isNotEmpty; + final List visibleSchedules = schedules.length <= 5 + ? schedules + : schedules.take(3).toList(); + final int remainingSchedules = + schedules.length <= 5 ? 0 : schedules.length - 3; + final String scheduleRange = hasSchedules + ? () { + final first = schedules.first.date; + final last = schedules.last.date; + if (first == last) { + return _formatDate(first); + } + return '${_formatDate(first)} – ${_formatDate(last)}'; + }() + : ''; + return GestureDetector( onTap: () { Modular.to.pushNamed( @@ -299,7 +324,55 @@ class _MyShiftCardState extends State { const SizedBox(height: UiConstants.space2), // Date & Time - if (widget.shift.durationDays != null && + if (hasSchedules) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + UiIcons.clock, + size: UiConstants.iconXs, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space1), + Text( + '${schedules.length} schedules', + style: UiTypography.footnote2m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + const SizedBox(height: UiConstants.space1), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + scheduleRange, + style: UiTypography.footnote2r.copyWith(color: UiColors.primary), + ), + ), + ...visibleSchedules.map( + (schedule) => Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + '${_formatDate(schedule.date)}, ${_formatTime(schedule.startTime)} – ${_formatTime(schedule.endTime)}', + style: UiTypography.footnote2r.copyWith( + color: UiColors.primary, + ), + ), + ), + ), + if (remainingSchedules > 0) + Text( + '+$remainingSchedules more schedules', + style: UiTypography.footnote2r.copyWith( + color: UiColors.primary.withOpacity(0.7), + ), + ), + ], + ), + ] else if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) ...[ Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -324,17 +397,22 @@ class _MyShiftCardState extends State { ), const SizedBox(height: UiConstants.space1), Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - '${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} – ${_formatTime(widget.shift.endTime)}', - style: UiTypography.footnote2r.copyWith(color: UiColors.primary), + padding: const EdgeInsets.only(bottom: 2), + child: Text( + '${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} – ${_formatTime(widget.shift.endTime)}', + style: UiTypography.footnote2r.copyWith( + color: UiColors.primary, ), + ), ), if (widget.shift.durationDays! > 1) - Text( - '... +${widget.shift.durationDays! - 1} more days', - style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)), - ) + Text( + '... +${widget.shift.durationDays! - 1} more days', + style: UiTypography.footnote2r.copyWith( + color: + UiColors.primary.withOpacity(0.7), + ), + ) ], ), ] else ...[ 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 bb426fd7..81e6ac03 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 @@ -20,6 +20,119 @@ class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; + bool _isRecurring(Shift shift) => + (shift.orderType ?? '').toUpperCase() == 'RECURRING'; + + bool _isPermanent(Shift shift) => + (shift.orderType ?? '').toUpperCase() == 'PERMANENT'; + + DateTime? _parseShiftDate(String date) { + if (date.isEmpty) return null; + try { + return DateTime.parse(date); + } catch (_) { + return null; + } + } + + List _groupMultiDayShifts(List shifts) { + final Map> grouped = >{}; + for (final shift in shifts) { + if (!_isRecurring(shift) && !_isPermanent(shift)) { + continue; + } + final orderId = shift.orderId; + final roleId = shift.roleId; + if (orderId == null || roleId == null) { + continue; + } + final key = '$orderId::$roleId'; + grouped.putIfAbsent(key, () => []).add(shift); + } + + final Set addedGroups = {}; + final List result = []; + + for (final shift in shifts) { + if (!_isRecurring(shift) && !_isPermanent(shift)) { + result.add(shift); + continue; + } + final orderId = shift.orderId; + final roleId = shift.roleId; + if (orderId == null || roleId == null) { + result.add(shift); + continue; + } + final key = '$orderId::$roleId'; + if (addedGroups.contains(key)) { + continue; + } + addedGroups.add(key); + final List group = grouped[key] ?? []; + if (group.isEmpty) { + result.add(shift); + continue; + } + group.sort((a, b) { + final ad = _parseShiftDate(a.date); + final bd = _parseShiftDate(b.date); + if (ad == null && bd == null) return 0; + if (ad == null) return 1; + if (bd == null) return -1; + return ad.compareTo(bd); + }); + + final Shift first = group.first; + final List schedules = group + .map((s) => ShiftSchedule( + date: s.date, + startTime: s.startTime, + endTime: s.endTime, + )) + .toList(); + + result.add( + Shift( + id: first.id, + roleId: first.roleId, + title: first.title, + clientName: first.clientName, + logoUrl: first.logoUrl, + hourlyRate: first.hourlyRate, + location: first.location, + locationAddress: first.locationAddress, + date: first.date, + startTime: first.startTime, + endTime: first.endTime, + createdDate: first.createdDate, + tipsAvailable: first.tipsAvailable, + travelTime: first.travelTime, + mealProvided: first.mealProvided, + parkingAvailable: first.parkingAvailable, + gasCompensation: first.gasCompensation, + description: first.description, + instructions: first.instructions, + managers: first.managers, + latitude: first.latitude, + longitude: first.longitude, + status: first.status, + durationDays: schedules.length, + requiredSlots: first.requiredSlots, + filledSlots: first.filledSlots, + hasApplied: first.hasApplied, + totalValue: first.totalValue, + breakInfo: first.breakInfo, + orderId: first.orderId, + orderType: first.orderType, + schedules: schedules, + ), + ); + } + + return result; + } + Widget _buildFilterTab(String id, String label) { final isSelected = _jobType == id; return GestureDetector( @@ -49,8 +162,10 @@ class _FindShiftsTabState extends State { @override Widget build(BuildContext context) { + final groupedJobs = _groupMultiDayShifts(widget.availableJobs); + // Filter logic - final filteredJobs = widget.availableJobs.where((s) { + final filteredJobs = groupedJobs.where((s) { final matchesSearch = s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || @@ -60,10 +175,15 @@ class _FindShiftsTabState extends State { if (_jobType == 'all') return true; if (_jobType == 'one-day') { + if (_isRecurring(s) || _isPermanent(s)) return false; return s.durationDays == null || s.durationDays! <= 1; } if (_jobType == 'multi-day') { - return s.durationDays != null && s.durationDays! > 1; + return _isRecurring(s) || + (s.durationDays != null && s.durationDays! > 1); + } + if (_jobType == 'long-term') { + return _isPermanent(s); } return true; }).toList();