From 4cd83a92811cf57bcde0f0468d69fa9097ceeb95 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 19 Mar 2026 23:34:29 -0400 Subject: [PATCH] feat: Implement UTC conversion for order date and time serialization in order use cases --- .../core/lib/src/utils/time_utils.dart | 42 +++++++++++++++ .../arguments/one_time_order_arguments.dart | 33 ++++++++++++ .../arguments/permanent_order_arguments.dart | 45 ++++++++++++++++ .../arguments/recurring_order_arguments.dart | 47 ++++++++++++++++ .../create_one_time_order_usecase.dart | 40 ++------------ .../create_permanent_order_usecase.dart | 52 ++---------------- .../create_recurring_order_usecase.dart | 54 ++----------------- .../view_orders_repository_impl.dart | 4 +- .../shifts_repository_impl.dart | 4 +- 9 files changed, 186 insertions(+), 135 deletions(-) diff --git a/apps/mobile/packages/core/lib/src/utils/time_utils.dart b/apps/mobile/packages/core/lib/src/utils/time_utils.dart index 0f5b7d8c..f8e25eb2 100644 --- a/apps/mobile/packages/core/lib/src/utils/time_utils.dart +++ b/apps/mobile/packages/core/lib/src/utils/time_utils.dart @@ -68,3 +68,45 @@ String formatTime(String timeStr) { } } } + +/// Converts a local date + local HH:MM time string to a UTC HH:MM string. +/// +/// Combines [localDate] with the hours and minutes from [localTime] (e.g. +/// "09:00") to create a full local [DateTime], converts it to UTC, then +/// extracts the HH:MM portion. +/// +/// Example: March 19, "21:00" in UTC-5 → "02:00" (next day UTC). +String toUtcTimeHHmm(DateTime localDate, String localTime) { + final List parts = localTime.split(':'); + final DateTime localDt = DateTime( + localDate.year, + localDate.month, + localDate.day, + int.parse(parts[0]), + int.parse(parts[1]), + ); + final DateTime utcDt = localDt.toUtc(); + return '${utcDt.hour.toString().padLeft(2, '0')}:' + '${utcDt.minute.toString().padLeft(2, '0')}'; +} + +/// Converts a local date + local HH:MM time string to a UTC YYYY-MM-DD string. +/// +/// This accounts for date-boundary crossings: a shift at 11 PM on March 19 +/// in UTC-5 is actually March 20 in UTC. +/// +/// Example: March 19, "23:00" in UTC-5 → "2026-03-20". +String toUtcDateIso(DateTime localDate, String localTime) { + final List parts = localTime.split(':'); + final DateTime localDt = DateTime( + localDate.year, + localDate.month, + localDate.day, + int.parse(parts[0]), + int.parse(parts[1]), + ); + final DateTime utcDt = localDt.toUtc(); + return '${utcDt.year.toString().padLeft(4, '0')}-' + '${utcDt.month.toString().padLeft(2, '0')}-' + '${utcDt.day.toString().padLeft(2, '0')}'; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/one_time_order_arguments.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/one_time_order_arguments.dart index 890fbeaf..308e74b1 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/one_time_order_arguments.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/one_time_order_arguments.dart @@ -63,6 +63,39 @@ class OneTimeOrderArguments extends UseCaseArgument { /// The selected vendor ID, if applicable. final String? vendorId; + /// Serialises these arguments into the V2 API payload shape. + /// + /// Times and dates are converted to UTC so the backend's + /// `combineDateAndTime` helper receives the correct values. + Map toJson() { + final String firstStartTime = + positions.isNotEmpty ? positions.first.startTime : '00:00'; + final String utcOrderDate = toUtcDateIso(orderDate, firstStartTime); + + final List> positionsList = + positions.map((OneTimeOrderPositionArgument p) { + return { + if (p.roleName != null) 'roleName': p.roleName, + if (p.roleId.isNotEmpty) 'roleId': p.roleId, + 'workerCount': p.workerCount, + 'startTime': toUtcTimeHHmm(orderDate, p.startTime), + 'endTime': toUtcTimeHHmm(orderDate, p.endTime), + if (p.lunchBreak != null && + p.lunchBreak != 'NO_BREAK' && + p.lunchBreak!.isNotEmpty) + 'lunchBreakMinutes': breakMinutesFromLabel(p.lunchBreak!), + }; + }).toList(); + + return { + 'hubId': hubId, + 'eventName': eventName, + 'orderDate': utcOrderDate, + 'positions': positionsList, + if (vendorId != null) 'vendorId': vendorId, + }; + } + @override List get props => [hubId, eventName, orderDate, positions, vendorId]; diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/permanent_order_arguments.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/permanent_order_arguments.dart index fb19864e..859097fd 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/permanent_order_arguments.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/permanent_order_arguments.dart @@ -63,6 +63,51 @@ class PermanentOrderArguments extends UseCaseArgument { /// The selected vendor ID, if applicable. final String? vendorId; + /// Day-of-week labels in Sunday-first order, matching the V2 API convention. + static const List _dayLabels = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', + ]; + + /// Serialises these arguments into the V2 API payload shape. + /// + /// Times and dates are converted to UTC so the backend's + /// `combineDateAndTime` helper receives the correct values. + Map toJson() { + final String firstStartTime = + positions.isNotEmpty ? positions.first.startTime : '00:00'; + final String utcStartDate = toUtcDateIso(startDate, firstStartTime); + + final List daysOfWeekList = daysOfWeek + .map((String day) => _dayLabels.indexOf(day) % 7) + .toList(); + + final List> positionsList = + positions.map((PermanentOrderPositionArgument p) { + return { + if (p.roleName != null) 'roleName': p.roleName, + if (p.roleId.isNotEmpty) 'roleId': p.roleId, + 'workerCount': p.workerCount, + 'startTime': toUtcTimeHHmm(startDate, p.startTime), + 'endTime': toUtcTimeHHmm(startDate, p.endTime), + }; + }).toList(); + + return { + 'hubId': hubId, + 'eventName': eventName, + 'startDate': utcStartDate, + 'daysOfWeek': daysOfWeekList, + 'positions': positionsList, + if (vendorId != null) 'vendorId': vendorId, + }; + } + @override List get props => [ hubId, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/recurring_order_arguments.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/recurring_order_arguments.dart index 01999078..ef219e07 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/recurring_order_arguments.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/arguments/recurring_order_arguments.dart @@ -67,6 +67,53 @@ class RecurringOrderArguments extends UseCaseArgument { /// The selected vendor ID, if applicable. final String? vendorId; + /// Day-of-week labels in Sunday-first order, matching the V2 API convention. + static const List _dayLabels = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', + ]; + + /// Serialises these arguments into the V2 API payload shape. + /// + /// Times and dates are converted to UTC so the backend's + /// `combineDateAndTime` helper receives the correct values. + Map toJson() { + final String firstStartTime = + positions.isNotEmpty ? positions.first.startTime : '00:00'; + final String utcStartDate = toUtcDateIso(startDate, firstStartTime); + final String utcEndDate = toUtcDateIso(endDate, firstStartTime); + + final List recurrenceDaysList = recurringDays + .map((String day) => _dayLabels.indexOf(day) % 7) + .toList(); + + final List> positionsList = + positions.map((RecurringOrderPositionArgument p) { + return { + if (p.roleName != null) 'roleName': p.roleName, + if (p.roleId.isNotEmpty) 'roleId': p.roleId, + 'workerCount': p.workerCount, + 'startTime': toUtcTimeHHmm(startDate, p.startTime), + 'endTime': toUtcTimeHHmm(startDate, p.endTime), + }; + }).toList(); + + return { + 'hubId': hubId, + 'eventName': eventName, + 'startDate': utcStartDate, + 'endDate': utcEndDate, + 'recurrenceDays': recurrenceDaysList, + 'positions': positionsList, + if (vendorId != null) 'vendorId': vendorId, + }; + } + @override List get props => [ hubId, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart index f74c4b63..eea3fdbc 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart @@ -1,49 +1,19 @@ -import 'package:krow_core/core.dart'; - import '../arguments/one_time_order_arguments.dart'; import '../repositories/client_create_order_repository_interface.dart'; /// Use case for creating a one-time staffing order. /// -/// Builds the V2 API payload from typed [OneTimeOrderArguments] and -/// delegates submission to the repository. Payload construction (date -/// formatting, position mapping, break-minutes conversion) is business -/// logic that belongs here, not in the BLoC. -class CreateOneTimeOrderUseCase - implements UseCase { +/// Delegates payload construction to [OneTimeOrderArguments.toJson] and +/// submission to the repository. +class CreateOneTimeOrderUseCase { /// Creates a [CreateOneTimeOrderUseCase]. const CreateOneTimeOrderUseCase(this._repository); /// The create-order repository. final ClientCreateOrderRepositoryInterface _repository; - @override + /// Creates a one-time order from the given arguments. Future call(OneTimeOrderArguments input) { - final String orderDate = formatDateToIso(input.orderDate); - - final List> positions = - input.positions.map((OneTimeOrderPositionArgument p) { - return { - if (p.roleName != null) 'roleName': p.roleName, - if (p.roleId.isNotEmpty) 'roleId': p.roleId, - 'workerCount': p.workerCount, - 'startTime': p.startTime, - 'endTime': p.endTime, - if (p.lunchBreak != null && - p.lunchBreak != 'NO_BREAK' && - p.lunchBreak!.isNotEmpty) - 'lunchBreakMinutes': breakMinutesFromLabel(p.lunchBreak!), - }; - }).toList(); - - final Map payload = { - 'hubId': input.hubId, - 'eventName': input.eventName, - 'orderDate': orderDate, - 'positions': positions, - if (input.vendorId != null) 'vendorId': input.vendorId, - }; - - return _repository.createOneTimeOrder(payload); + return _repository.createOneTimeOrder(input.toJson()); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart index e33163d9..970ea149 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_permanent_order_usecase.dart @@ -1,61 +1,19 @@ -import 'package:krow_core/core.dart'; - import '../arguments/permanent_order_arguments.dart'; import '../repositories/client_create_order_repository_interface.dart'; -/// Day-of-week labels in Sunday-first order, matching the V2 API convention. -const List _dayLabels = [ - 'SUN', - 'MON', - 'TUE', - 'WED', - 'THU', - 'FRI', - 'SAT', -]; - /// Use case for creating a permanent staffing order. /// -/// Builds the V2 API payload from typed [PermanentOrderArguments] and -/// delegates submission to the repository. Payload construction (date -/// formatting, day-of-week mapping, position mapping) is business -/// logic that belongs here, not in the BLoC. -class CreatePermanentOrderUseCase - implements UseCase { +/// Delegates payload construction to [PermanentOrderArguments.toJson] and +/// submission to the repository. +class CreatePermanentOrderUseCase { /// Creates a [CreatePermanentOrderUseCase]. const CreatePermanentOrderUseCase(this._repository); /// The create-order repository. final ClientCreateOrderRepositoryInterface _repository; - @override + /// Creates a permanent order from the given arguments. Future call(PermanentOrderArguments input) { - final String startDate = formatDateToIso(input.startDate); - - final List daysOfWeek = input.daysOfWeek - .map((String day) => _dayLabels.indexOf(day) % 7) - .toList(); - - final List> positions = - input.positions.map((PermanentOrderPositionArgument p) { - return { - if (p.roleName != null) 'roleName': p.roleName, - if (p.roleId.isNotEmpty) 'roleId': p.roleId, - 'workerCount': p.workerCount, - 'startTime': p.startTime, - 'endTime': p.endTime, - }; - }).toList(); - - final Map payload = { - 'hubId': input.hubId, - 'eventName': input.eventName, - 'startDate': startDate, - 'daysOfWeek': daysOfWeek, - 'positions': positions, - if (input.vendorId != null) 'vendorId': input.vendorId, - }; - - return _repository.createPermanentOrder(payload); + return _repository.createPermanentOrder(input.toJson()); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart index 7bd1232f..48d26c78 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/create_recurring_order_usecase.dart @@ -1,63 +1,19 @@ -import 'package:krow_core/core.dart'; - import '../arguments/recurring_order_arguments.dart'; import '../repositories/client_create_order_repository_interface.dart'; -/// Day-of-week labels in Sunday-first order, matching the V2 API convention. -const List _dayLabels = [ - 'SUN', - 'MON', - 'TUE', - 'WED', - 'THU', - 'FRI', - 'SAT', -]; - /// Use case for creating a recurring staffing order. /// -/// Builds the V2 API payload from typed [RecurringOrderArguments] and -/// delegates submission to the repository. Payload construction (date -/// formatting, recurrence-day mapping, position mapping) is business -/// logic that belongs here, not in the BLoC. -class CreateRecurringOrderUseCase - implements UseCase { +/// Delegates payload construction to [RecurringOrderArguments.toJson] and +/// submission to the repository. +class CreateRecurringOrderUseCase { /// Creates a [CreateRecurringOrderUseCase]. const CreateRecurringOrderUseCase(this._repository); /// The create-order repository. final ClientCreateOrderRepositoryInterface _repository; - @override + /// Creates a recurring order from the given arguments. Future call(RecurringOrderArguments input) { - final String startDate = formatDateToIso(input.startDate); - final String endDate = formatDateToIso(input.endDate); - - final List recurrenceDays = input.recurringDays - .map((String day) => _dayLabels.indexOf(day) % 7) - .toList(); - - final List> positions = - input.positions.map((RecurringOrderPositionArgument p) { - return { - if (p.roleName != null) 'roleName': p.roleName, - if (p.roleId.isNotEmpty) 'roleId': p.roleId, - 'workerCount': p.workerCount, - 'startTime': p.startTime, - 'endTime': p.endTime, - }; - }).toList(); - - final Map payload = { - 'hubId': input.hubId, - 'eventName': input.eventName, - 'startDate': startDate, - 'endDate': endDate, - 'recurrenceDays': recurrenceDays, - 'positions': positions, - if (input.vendorId != null) 'vendorId': input.vendorId, - }; - - return _repository.createRecurringOrder(payload); + return _repository.createRecurringOrder(input.toJson()); } } diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart index 192b4384..91967d92 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart @@ -22,8 +22,8 @@ class ViewOrdersRepositoryImpl implements ViewOrdersRepositoryInterface { final ApiResponse response = await _api.get( ClientEndpoints.ordersView, params: { - 'startDate': start.toIso8601String(), - 'endDate': end.toIso8601String(), + 'startDate': start.toUtc().toIso8601String(), + 'endDate': end.toUtc().toIso8601String(), }, ); final Map data = response.data as Map; 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 2ade65ba..e5a118af 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 @@ -36,8 +36,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { final ApiResponse response = await _apiService.get( StaffEndpoints.shiftsAssigned, params: { - 'startDate': start.toIso8601String(), - 'endDate': end.toIso8601String(), + 'startDate': start.toUtc().toIso8601String(), + 'endDate': end.toUtc().toIso8601String(), }, ); final List items = _extractItems(response.data);