feat: Implement UTC conversion for order date and time serialization in order use cases
This commit is contained in:
@@ -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<String> 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<String> 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')}';
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,39 @@ class OneTimeOrderArguments extends UseCaseArgument {
|
|||||||
/// The selected vendor ID, if applicable.
|
/// The selected vendor ID, if applicable.
|
||||||
final String? vendorId;
|
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<String, dynamic> toJson() {
|
||||||
|
final String firstStartTime =
|
||||||
|
positions.isNotEmpty ? positions.first.startTime : '00:00';
|
||||||
|
final String utcOrderDate = toUtcDateIso(orderDate, firstStartTime);
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> positionsList =
|
||||||
|
positions.map((OneTimeOrderPositionArgument p) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
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 <String, dynamic>{
|
||||||
|
'hubId': hubId,
|
||||||
|
'eventName': eventName,
|
||||||
|
'orderDate': utcOrderDate,
|
||||||
|
'positions': positionsList,
|
||||||
|
if (vendorId != null) 'vendorId': vendorId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props =>
|
List<Object?> get props =>
|
||||||
<Object?>[hubId, eventName, orderDate, positions, vendorId];
|
<Object?>[hubId, eventName, orderDate, positions, vendorId];
|
||||||
|
|||||||
@@ -63,6 +63,51 @@ class PermanentOrderArguments extends UseCaseArgument {
|
|||||||
/// The selected vendor ID, if applicable.
|
/// The selected vendor ID, if applicable.
|
||||||
final String? vendorId;
|
final String? vendorId;
|
||||||
|
|
||||||
|
/// Day-of-week labels in Sunday-first order, matching the V2 API convention.
|
||||||
|
static const List<String> _dayLabels = <String>[
|
||||||
|
'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<String, dynamic> toJson() {
|
||||||
|
final String firstStartTime =
|
||||||
|
positions.isNotEmpty ? positions.first.startTime : '00:00';
|
||||||
|
final String utcStartDate = toUtcDateIso(startDate, firstStartTime);
|
||||||
|
|
||||||
|
final List<int> daysOfWeekList = daysOfWeek
|
||||||
|
.map((String day) => _dayLabels.indexOf(day) % 7)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> positionsList =
|
||||||
|
positions.map((PermanentOrderPositionArgument p) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
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 <String, dynamic>{
|
||||||
|
'hubId': hubId,
|
||||||
|
'eventName': eventName,
|
||||||
|
'startDate': utcStartDate,
|
||||||
|
'daysOfWeek': daysOfWeekList,
|
||||||
|
'positions': positionsList,
|
||||||
|
if (vendorId != null) 'vendorId': vendorId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
hubId,
|
hubId,
|
||||||
|
|||||||
@@ -67,6 +67,53 @@ class RecurringOrderArguments extends UseCaseArgument {
|
|||||||
/// The selected vendor ID, if applicable.
|
/// The selected vendor ID, if applicable.
|
||||||
final String? vendorId;
|
final String? vendorId;
|
||||||
|
|
||||||
|
/// Day-of-week labels in Sunday-first order, matching the V2 API convention.
|
||||||
|
static const List<String> _dayLabels = <String>[
|
||||||
|
'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<String, dynamic> 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<int> recurrenceDaysList = recurringDays
|
||||||
|
.map((String day) => _dayLabels.indexOf(day) % 7)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> positionsList =
|
||||||
|
positions.map((RecurringOrderPositionArgument p) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
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 <String, dynamic>{
|
||||||
|
'hubId': hubId,
|
||||||
|
'eventName': eventName,
|
||||||
|
'startDate': utcStartDate,
|
||||||
|
'endDate': utcEndDate,
|
||||||
|
'recurrenceDays': recurrenceDaysList,
|
||||||
|
'positions': positionsList,
|
||||||
|
if (vendorId != null) 'vendorId': vendorId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
hubId,
|
hubId,
|
||||||
|
|||||||
@@ -1,49 +1,19 @@
|
|||||||
import 'package:krow_core/core.dart';
|
|
||||||
|
|
||||||
import '../arguments/one_time_order_arguments.dart';
|
import '../arguments/one_time_order_arguments.dart';
|
||||||
import '../repositories/client_create_order_repository_interface.dart';
|
import '../repositories/client_create_order_repository_interface.dart';
|
||||||
|
|
||||||
/// Use case for creating a one-time staffing order.
|
/// Use case for creating a one-time staffing order.
|
||||||
///
|
///
|
||||||
/// Builds the V2 API payload from typed [OneTimeOrderArguments] and
|
/// Delegates payload construction to [OneTimeOrderArguments.toJson] and
|
||||||
/// delegates submission to the repository. Payload construction (date
|
/// submission to the repository.
|
||||||
/// formatting, position mapping, break-minutes conversion) is business
|
class CreateOneTimeOrderUseCase {
|
||||||
/// logic that belongs here, not in the BLoC.
|
|
||||||
class CreateOneTimeOrderUseCase
|
|
||||||
implements UseCase<OneTimeOrderArguments, void> {
|
|
||||||
/// Creates a [CreateOneTimeOrderUseCase].
|
/// Creates a [CreateOneTimeOrderUseCase].
|
||||||
const CreateOneTimeOrderUseCase(this._repository);
|
const CreateOneTimeOrderUseCase(this._repository);
|
||||||
|
|
||||||
/// The create-order repository.
|
/// The create-order repository.
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
@override
|
/// Creates a one-time order from the given arguments.
|
||||||
Future<void> call(OneTimeOrderArguments input) {
|
Future<void> call(OneTimeOrderArguments input) {
|
||||||
final String orderDate = formatDateToIso(input.orderDate);
|
return _repository.createOneTimeOrder(input.toJson());
|
||||||
|
|
||||||
final List<Map<String, dynamic>> positions =
|
|
||||||
input.positions.map((OneTimeOrderPositionArgument p) {
|
|
||||||
return <String, dynamic>{
|
|
||||||
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<String, dynamic> payload = <String, dynamic>{
|
|
||||||
'hubId': input.hubId,
|
|
||||||
'eventName': input.eventName,
|
|
||||||
'orderDate': orderDate,
|
|
||||||
'positions': positions,
|
|
||||||
if (input.vendorId != null) 'vendorId': input.vendorId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return _repository.createOneTimeOrder(payload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,19 @@
|
|||||||
import 'package:krow_core/core.dart';
|
|
||||||
|
|
||||||
import '../arguments/permanent_order_arguments.dart';
|
import '../arguments/permanent_order_arguments.dart';
|
||||||
import '../repositories/client_create_order_repository_interface.dart';
|
import '../repositories/client_create_order_repository_interface.dart';
|
||||||
|
|
||||||
/// Day-of-week labels in Sunday-first order, matching the V2 API convention.
|
|
||||||
const List<String> _dayLabels = <String>[
|
|
||||||
'SUN',
|
|
||||||
'MON',
|
|
||||||
'TUE',
|
|
||||||
'WED',
|
|
||||||
'THU',
|
|
||||||
'FRI',
|
|
||||||
'SAT',
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Use case for creating a permanent staffing order.
|
/// Use case for creating a permanent staffing order.
|
||||||
///
|
///
|
||||||
/// Builds the V2 API payload from typed [PermanentOrderArguments] and
|
/// Delegates payload construction to [PermanentOrderArguments.toJson] and
|
||||||
/// delegates submission to the repository. Payload construction (date
|
/// submission to the repository.
|
||||||
/// formatting, day-of-week mapping, position mapping) is business
|
class CreatePermanentOrderUseCase {
|
||||||
/// logic that belongs here, not in the BLoC.
|
|
||||||
class CreatePermanentOrderUseCase
|
|
||||||
implements UseCase<PermanentOrderArguments, void> {
|
|
||||||
/// Creates a [CreatePermanentOrderUseCase].
|
/// Creates a [CreatePermanentOrderUseCase].
|
||||||
const CreatePermanentOrderUseCase(this._repository);
|
const CreatePermanentOrderUseCase(this._repository);
|
||||||
|
|
||||||
/// The create-order repository.
|
/// The create-order repository.
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
@override
|
/// Creates a permanent order from the given arguments.
|
||||||
Future<void> call(PermanentOrderArguments input) {
|
Future<void> call(PermanentOrderArguments input) {
|
||||||
final String startDate = formatDateToIso(input.startDate);
|
return _repository.createPermanentOrder(input.toJson());
|
||||||
|
|
||||||
final List<int> daysOfWeek = input.daysOfWeek
|
|
||||||
.map((String day) => _dayLabels.indexOf(day) % 7)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final List<Map<String, dynamic>> positions =
|
|
||||||
input.positions.map((PermanentOrderPositionArgument p) {
|
|
||||||
return <String, dynamic>{
|
|
||||||
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<String, dynamic> payload = <String, dynamic>{
|
|
||||||
'hubId': input.hubId,
|
|
||||||
'eventName': input.eventName,
|
|
||||||
'startDate': startDate,
|
|
||||||
'daysOfWeek': daysOfWeek,
|
|
||||||
'positions': positions,
|
|
||||||
if (input.vendorId != null) 'vendorId': input.vendorId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return _repository.createPermanentOrder(payload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,19 @@
|
|||||||
import 'package:krow_core/core.dart';
|
|
||||||
|
|
||||||
import '../arguments/recurring_order_arguments.dart';
|
import '../arguments/recurring_order_arguments.dart';
|
||||||
import '../repositories/client_create_order_repository_interface.dart';
|
import '../repositories/client_create_order_repository_interface.dart';
|
||||||
|
|
||||||
/// Day-of-week labels in Sunday-first order, matching the V2 API convention.
|
|
||||||
const List<String> _dayLabels = <String>[
|
|
||||||
'SUN',
|
|
||||||
'MON',
|
|
||||||
'TUE',
|
|
||||||
'WED',
|
|
||||||
'THU',
|
|
||||||
'FRI',
|
|
||||||
'SAT',
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Use case for creating a recurring staffing order.
|
/// Use case for creating a recurring staffing order.
|
||||||
///
|
///
|
||||||
/// Builds the V2 API payload from typed [RecurringOrderArguments] and
|
/// Delegates payload construction to [RecurringOrderArguments.toJson] and
|
||||||
/// delegates submission to the repository. Payload construction (date
|
/// submission to the repository.
|
||||||
/// formatting, recurrence-day mapping, position mapping) is business
|
class CreateRecurringOrderUseCase {
|
||||||
/// logic that belongs here, not in the BLoC.
|
|
||||||
class CreateRecurringOrderUseCase
|
|
||||||
implements UseCase<RecurringOrderArguments, void> {
|
|
||||||
/// Creates a [CreateRecurringOrderUseCase].
|
/// Creates a [CreateRecurringOrderUseCase].
|
||||||
const CreateRecurringOrderUseCase(this._repository);
|
const CreateRecurringOrderUseCase(this._repository);
|
||||||
|
|
||||||
/// The create-order repository.
|
/// The create-order repository.
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
@override
|
/// Creates a recurring order from the given arguments.
|
||||||
Future<void> call(RecurringOrderArguments input) {
|
Future<void> call(RecurringOrderArguments input) {
|
||||||
final String startDate = formatDateToIso(input.startDate);
|
return _repository.createRecurringOrder(input.toJson());
|
||||||
final String endDate = formatDateToIso(input.endDate);
|
|
||||||
|
|
||||||
final List<int> recurrenceDays = input.recurringDays
|
|
||||||
.map((String day) => _dayLabels.indexOf(day) % 7)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final List<Map<String, dynamic>> positions =
|
|
||||||
input.positions.map((RecurringOrderPositionArgument p) {
|
|
||||||
return <String, dynamic>{
|
|
||||||
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<String, dynamic> payload = <String, dynamic>{
|
|
||||||
'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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ class ViewOrdersRepositoryImpl implements ViewOrdersRepositoryInterface {
|
|||||||
final ApiResponse response = await _api.get(
|
final ApiResponse response = await _api.get(
|
||||||
ClientEndpoints.ordersView,
|
ClientEndpoints.ordersView,
|
||||||
params: <String, dynamic>{
|
params: <String, dynamic>{
|
||||||
'startDate': start.toIso8601String(),
|
'startDate': start.toUtc().toIso8601String(),
|
||||||
'endDate': end.toIso8601String(),
|
'endDate': end.toUtc().toIso8601String(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
final ApiResponse response = await _apiService.get(
|
final ApiResponse response = await _apiService.get(
|
||||||
StaffEndpoints.shiftsAssigned,
|
StaffEndpoints.shiftsAssigned,
|
||||||
params: <String, dynamic>{
|
params: <String, dynamic>{
|
||||||
'startDate': start.toIso8601String(),
|
'startDate': start.toUtc().toIso8601String(),
|
||||||
'endDate': end.toIso8601String(),
|
'endDate': end.toUtc().toIso8601String(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final List<dynamic> items = _extractItems(response.data);
|
final List<dynamic> items = _extractItems(response.data);
|
||||||
|
|||||||
Reference in New Issue
Block a user