From 9b6cad3bdecfda73d3a4514f03b3828359262f82 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Mon, 16 Feb 2026 13:26:04 -0500 Subject: [PATCH] feat(breaks): Implement break functionality with Break entity and adapter --- .../packages/domain/lib/krow_domain.dart | 2 + .../adapters/shifts/break/break_adapter.dart | 39 ++++++++ .../lib/src/entities/shifts/break/break.dart | 47 ++++++++++ .../domain/lib/src/entities/shifts/shift.dart | 71 ++++++++------- .../shifts_repository_impl.dart | 24 +++++ .../pages/shift_details_page.dart | 90 ++++++------------- .../connector/application/queries.gql | 7 +- 7 files changed, 180 insertions(+), 100 deletions(-) create mode 100644 apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 3dc41679..bbe513ae 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -30,6 +30,8 @@ export 'src/entities/events/work_session.dart'; // Shifts export 'src/entities/shifts/shift.dart'; export 'src/adapters/shifts/shift_adapter.dart'; +export 'src/entities/shifts/break/break.dart'; +export 'src/adapters/shifts/break/break_adapter.dart'; // Orders & Requests export 'src/entities/orders/order_type.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart new file mode 100644 index 00000000..59f46949 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/shifts/break/break_adapter.dart @@ -0,0 +1,39 @@ +import '../../../entities/shifts/break/break.dart'; + +/// Adapter for Break related data. +class BreakAdapter { + /// Maps break data to a Break entity. + /// + /// [isPaid] whether the break is paid. + /// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30'). + static Break fromData({ + required bool isPaid, + required String? breakTime, + }) { + return Break( + isBreakPaid: isPaid, + duration: _parseDuration(breakTime), + ); + } + + static BreakDuration _parseDuration(String? breakTime) { + if (breakTime == null) return BreakDuration.none; + + switch (breakTime.toUpperCase()) { + case 'MIN_10': + return BreakDuration.ten; + case 'MIN_15': + return BreakDuration.fifteen; + case 'MIN_20': + return BreakDuration.twenty; + case 'MIN_30': + return BreakDuration.thirty; + case 'MIN_45': + return BreakDuration.fortyFive; + case 'MIN_60': + return BreakDuration.sixty; + default: + return BreakDuration.none; + } + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart new file mode 100644 index 00000000..b90750bd --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/break/break.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; + +/// Enum representing common break durations in minutes. +enum BreakDuration { + /// No break. + none(0), + + /// 10 minutes break. + ten(10), + + /// 15 minutes break. + fifteen(15), + + /// 20 minutes break. + twenty(20), + + /// 30 minutes break. + thirty(30), + + /// 45 minutes break. + fortyFive(45), + + /// 60 minutes break. + sixty(60); + + /// The duration in minutes. + final int minutes; + + const BreakDuration(this.minutes); +} + +/// Represents a break configuration for a shift. +class Break extends Equatable { + const Break({ + required this.duration, + required this.isBreakPaid, + }); + + /// The duration of the break. + final BreakDuration duration; + + /// Whether the break is paid or unpaid. + final bool isBreakPaid; + + @override + List get props => [duration, isBreakPaid]; +} 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 5ef22e1e..e24d6477 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:krow_domain/src/entities/shifts/break/break.dart'; class Shift extends Equatable { final String id; @@ -29,6 +30,7 @@ class Shift extends Equatable { final String? roleId; final bool? hasApplied; final double? totalValue; + final Break? breakInfo; const Shift({ required this.id, @@ -59,48 +61,49 @@ class Shift extends Equatable { this.roleId, this.hasApplied, this.totalValue, + this.breakInfo, }); @override - List get props => [ - id, - title, - clientName, - logoUrl, - hourlyRate, - location, - locationAddress, - date, - startTime, - endTime, - createdDate, - tipsAvailable, - travelTime, - mealProvided, - parkingAvailable, - gasCompensation, - description, - instructions, - managers, - latitude, - longitude, - status, - durationDays, - requiredSlots, - filledSlots, - roleId, - hasApplied, - totalValue, - ]; + List get props => [ + id, + title, + clientName, + logoUrl, + hourlyRate, + location, + locationAddress, + date, + startTime, + endTime, + createdDate, + tipsAvailable, + travelTime, + mealProvided, + parkingAvailable, + gasCompensation, + description, + instructions, + managers, + latitude, + longitude, + status, + durationDays, + requiredSlots, + filledSlots, + roleId, + hasApplied, + totalValue, + breakInfo, + ]; } class ShiftManager extends Equatable { + const ShiftManager({required this.name, required this.phone, this.avatar}); + final String name; final String phone; final String? avatar; - - const ShiftManager({required this.name, required this.phone, this.avatar}); - @override - List get props => [name, phone, avatar]; + List get props => [name, phone, avatar]; } 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 a1588633..5700c60a 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 @@ -141,6 +141,10 @@ class ShiftsRepositoryImpl requiredSlots: app.shiftRole.count, filledSlots: app.shiftRole.assigned ?? 0, hasApplied: true, + breakInfo: BreakAdapter.fromData( + isPaid: app.shiftRole.isBreakPaid ?? false, + breakTime: app.shiftRole.breakType?.stringValue, + ), ), ); } @@ -208,6 +212,10 @@ class ShiftsRepositoryImpl requiredSlots: app.shiftRole.count, filledSlots: app.shiftRole.assigned ?? 0, hasApplied: true, + breakInfo: BreakAdapter.fromData( + isPaid: app.shiftRole.isBreakPaid ?? false, + breakTime: app.shiftRole.breakType?.stringValue, + ), ), ); } @@ -277,6 +285,10 @@ class ShiftsRepositoryImpl durationDays: sr.shift.durationDays, requiredSlots: sr.count, filledSlots: sr.assigned ?? 0, + breakInfo: BreakAdapter.fromData( + isPaid: sr.isBreakPaid ?? false, + breakTime: sr.breakType?.stringValue, + ), ), ); } @@ -350,6 +362,10 @@ class ShiftsRepositoryImpl filledSlots: sr.assigned ?? 0, hasApplied: hasApplied, totalValue: sr.totalValue, + breakInfo: BreakAdapter.fromData( + isPaid: sr.isBreakPaid ?? false, + breakTime: sr.breakType?.stringValue, + ), ); } @@ -360,6 +376,7 @@ class ShiftsRepositoryImpl int? required; int? filled; + Break? breakInfo; try { final rolesRes = await executeProtected(() => _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); @@ -370,6 +387,12 @@ class ShiftsRepositoryImpl required = (required ?? 0) + r.count; filled = (filled ?? 0) + (r.assigned ?? 0); } + // Use the first role's break info as a representative + final firstRole = rolesRes.data.shiftRoles.first; + breakInfo = BreakAdapter.fromData( + isPaid: firstRole.isBreakPaid ?? false, + breakTime: firstRole.breakType?.stringValue, + ); } } catch (_) {} @@ -394,6 +417,7 @@ class ShiftsRepositoryImpl durationDays: s.durationDays, requiredSlots: required, filledSlots: filled, + breakInfo: breakInfo, ); } 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 7b8b05a5..7ca8f8ca 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 @@ -132,7 +132,7 @@ class _ShiftDetailsPageState extends State { return BlocProvider( create: (_) => Modular.get() ..add( - LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift?.roleId), + LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId), ), child: BlocListener( listener: (context, state) { @@ -148,7 +148,7 @@ class _ShiftDetailsPageState extends State { ); Modular.to.toShifts(selectedDate: state.shiftDate); } else if (state is ShiftDetailsError) { - if (_isApplying || widget.shift == null) { + if (_isApplying) { UiSnackbar.show( context, message: translateErrorKey(state.message), @@ -240,7 +240,7 @@ class _ShiftDetailsPageState extends State { const Divider(height: 1, thickness: 0.5), - // Date Section + // Date & Time Section Padding( padding: const EdgeInsets.all(UiConstants.space5), child: Column( @@ -248,8 +248,7 @@ class _ShiftDetailsPageState extends State { children: [ Text( i18n.shift_date, - style: UiTypography - .titleUppercase4b + style: UiTypography.titleUppercase4b .textSecondary, ), const SizedBox(height: UiConstants.space2), @@ -268,6 +267,24 @@ class _ShiftDetailsPageState extends State { ), ], ), + const SizedBox(height: UiConstants.space4), + Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildTimeBox( + "CLOCK OUT TIME", + displayShift.endTime, + ), + ), + ], + ), ], ), ), @@ -308,30 +325,6 @@ class _ShiftDetailsPageState extends State { const Divider(height: 1, thickness: 0.5), - // Time Section (New) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - displayShift.startTime, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - displayShift.endTime, - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - // Location Section (New with Map) Padding( padding: const EdgeInsets.all(UiConstants.space5), @@ -344,7 +337,6 @@ class _ShiftDetailsPageState extends State { .titleUppercase4b .textSecondary, ), - const SizedBox(height: UiConstants.space3), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -366,12 +358,10 @@ class _ShiftDetailsPageState extends State { ).showSnackBar( SnackBar( content: Text( - displayShift! - .locationAddress + displayShift.locationAddress .isNotEmpty - ? displayShift! - .locationAddress - : displayShift!.location, + ? displayShift.locationAddress + : displayShift.location, ), duration: const Duration( seconds: 3, @@ -509,36 +499,6 @@ class _ShiftDetailsPageState extends State { ); } - void _declineShift(BuildContext context, String id) { - final i18n = Translations.of( - context, - ).staff_shifts.shift_details.decline_dialog; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: Text(i18n.title), - content: Text(i18n.message), - actions: [ - TextButton( - onPressed: () => Modular.to.pop(), - child: Text(Translations.of(context).common.cancel), - ), - TextButton( - onPressed: () { - BlocProvider.of( - context, - ).add(DeclineShiftDetailsEvent(id)); - }, - style: TextButton.styleFrom(foregroundColor: UiColors.destructive), - child: Text( - Translations.of(context).staff_shifts.shift_details.decline, - ), - ), - ], - ), - ); - } - void _showApplyingDialog(BuildContext context, Shift shift) { if (_actionDialogOpen) return; _actionDialogOpen = true; diff --git a/backend/dataconnect/connector/application/queries.gql b/backend/dataconnect/connector/application/queries.gql index 44c66795..6d57438f 100644 --- a/backend/dataconnect/connector/application/queries.gql +++ b/backend/dataconnect/connector/application/queries.gql @@ -52,6 +52,8 @@ query listApplications @auth(level: USER) { startTime endTime hours + breakType + isBreakPaid totalValue role { id @@ -341,6 +343,8 @@ query getApplicationsByStaffId( startTime endTime hours + breakType + isBreakPaid totalValue role { id @@ -352,7 +356,6 @@ query getApplicationsByStaffId( } } - query vaidateDayStaffApplication( $staffId: UUID! $offset: Int @@ -692,6 +695,8 @@ query listCompletedApplicationsByStaffId( startTime endTime hours + breakType + isBreakPaid totalValue role {