Merge 592-migrate-frontend-applications-to-v2-backend-and-database into feature/session-persistence-new
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'data/repositories_impl/client_create_order_repository_impl.dart';
|
||||
import 'data/repositories_impl/client_order_query_repository_impl.dart';
|
||||
import 'domain/repositories/client_create_order_repository_interface.dart';
|
||||
import 'domain/repositories/client_order_query_repository_interface.dart';
|
||||
import 'domain/usecases/create_one_time_order_usecase.dart';
|
||||
import 'domain/usecases/create_permanent_order_usecase.dart';
|
||||
import 'domain/usecases/create_recurring_order_usecase.dart';
|
||||
import 'domain/usecases/create_rapid_order_usecase.dart';
|
||||
import 'domain/usecases/create_recurring_order_usecase.dart';
|
||||
import 'domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'domain/usecases/parse_rapid_order_usecase.dart';
|
||||
import 'domain/usecases/transcribe_rapid_order_usecase.dart';
|
||||
@@ -24,19 +24,17 @@ import 'presentation/pages/review_order_page.dart';
|
||||
|
||||
/// Module for the Client Create Order feature.
|
||||
///
|
||||
/// This module orchestrates the dependency injection for the create order feature,
|
||||
/// connecting the domain use cases with their data layer implementations and
|
||||
/// presentation layer BLoCs.
|
||||
/// Uses [CoreModule] for [BaseApiService] injection (V2 API).
|
||||
class ClientCreateOrderModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule(), CoreModule()];
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
|
||||
() => ClientCreateOrderRepositoryImpl(
|
||||
service: i.get<dc.DataConnectService>(),
|
||||
apiService: i.get<BaseApiService>(),
|
||||
rapidOrderService: i.get<RapidOrderService>(),
|
||||
fileUploadService: i.get<FileUploadService>(),
|
||||
),
|
||||
@@ -44,7 +42,7 @@ class ClientCreateOrderModule extends Module {
|
||||
|
||||
i.addLazySingleton<ClientOrderQueryRepositoryInterface>(
|
||||
() => ClientOrderQueryRepositoryImpl(
|
||||
service: i.get<dc.DataConnectService>(),
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,793 +1,89 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClientCreateOrderRepositoryInterface].
|
||||
/// V2 API implementation of [ClientCreateOrderRepositoryInterface].
|
||||
///
|
||||
/// This implementation coordinates data access for order creation by [DataConnectService] from the shared
|
||||
/// Data Connect package.
|
||||
///
|
||||
/// It follows the KROW Clean Architecture by keeping the data layer focused
|
||||
/// on delegation and data mapping, without business logic.
|
||||
/// Each create method sends a single POST to the typed V2 endpoint.
|
||||
/// The backend handles shift and role creation internally.
|
||||
class ClientCreateOrderRepositoryImpl
|
||||
implements ClientCreateOrderRepositoryInterface {
|
||||
/// Creates an instance backed by the given [apiService].
|
||||
ClientCreateOrderRepositoryImpl({
|
||||
required dc.DataConnectService service,
|
||||
required BaseApiService apiService,
|
||||
required RapidOrderService rapidOrderService,
|
||||
required FileUploadService fileUploadService,
|
||||
}) : _service = service,
|
||||
_rapidOrderService = rapidOrderService,
|
||||
_fileUploadService = fileUploadService;
|
||||
}) : _api = apiService,
|
||||
_rapidOrderService = rapidOrderService,
|
||||
_fileUploadService = fileUploadService;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
final RapidOrderService _rapidOrderService;
|
||||
final FileUploadService _fileUploadService;
|
||||
|
||||
@override
|
||||
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final String? vendorId = order.vendorId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
throw Exception('Vendor is missing.');
|
||||
}
|
||||
final domain.OneTimeOrderHubDetails? hub = order.hub;
|
||||
if (hub == null || hub.id.isEmpty) {
|
||||
throw Exception('Hub is missing.');
|
||||
}
|
||||
|
||||
final DateTime orderDateOnly = DateTime(
|
||||
order.date.year,
|
||||
order.date.month,
|
||||
order.date.day,
|
||||
);
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.ONE_TIME,
|
||||
teamHubId: hub.id,
|
||||
)
|
||||
.vendorId(vendorId)
|
||||
.eventName(order.eventName)
|
||||
.status(dc.OrderStatus.POSTED)
|
||||
.date(orderTimestamp)
|
||||
.execute();
|
||||
|
||||
final String orderId = orderResult.data.order_insert.id;
|
||||
|
||||
final int workersNeeded = order.positions.fold<int>(
|
||||
0,
|
||||
(int sum, domain.OneTimeOrderPosition position) => sum + position.count,
|
||||
);
|
||||
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
|
||||
final double shiftCost = _calculateShiftCost(order);
|
||||
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(orderTimestamp)
|
||||
.location(hub.name)
|
||||
.locationAddress(hub.address)
|
||||
.latitude(hub.latitude)
|
||||
.longitude(hub.longitude)
|
||||
.placeId(hub.placeId)
|
||||
.city(hub.city)
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
.cost(shiftCost)
|
||||
.execute();
|
||||
|
||||
final String shiftId = shiftResult.data.shift_insert.id;
|
||||
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.date, position.startTime);
|
||||
final DateTime end = _parseTime(order.date, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
|
||||
await _service.connector
|
||||
.createShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: position.role,
|
||||
count: position.count,
|
||||
)
|
||||
.startTime(_service.toTimestamp(start))
|
||||
.endTime(_service.toTimestamp(normalizedEnd))
|
||||
.hours(hours)
|
||||
.breakType(_breakDurationFromValue(position.lunchBreak))
|
||||
.isBreakPaid(_isBreakPaid(position.lunchBreak))
|
||||
.totalValue(totalValue)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(AnyValue(<String>[shiftId]))
|
||||
.execute();
|
||||
});
|
||||
Future<void> createOneTimeOrder(Map<String, dynamic> payload) async {
|
||||
await _api.post(ClientEndpoints.ordersOneTime, data: payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createRecurringOrder(domain.RecurringOrder order) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final String? vendorId = order.vendorId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
throw Exception('Vendor is missing.');
|
||||
}
|
||||
final domain.RecurringOrderHubDetails? hub = order.hub;
|
||||
if (hub == null || hub.id.isEmpty) {
|
||||
throw Exception('Hub is missing.');
|
||||
}
|
||||
|
||||
final DateTime orderDateOnly = DateTime(
|
||||
order.startDate.year,
|
||||
order.startDate.month,
|
||||
order.startDate.day,
|
||||
);
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||
final Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
||||
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.RECURRING,
|
||||
teamHubId: hub.id,
|
||||
)
|
||||
.vendorId(vendorId)
|
||||
.eventName(order.eventName)
|
||||
.status(dc.OrderStatus.POSTED)
|
||||
.date(orderTimestamp)
|
||||
.startDate(startTimestamp)
|
||||
.endDate(endTimestamp)
|
||||
.recurringDays(order.recurringDays)
|
||||
.execute();
|
||||
|
||||
final String orderId = orderResult.data.order_insert.id;
|
||||
|
||||
// NOTE: Recurring orders are limited to 30 days of generated shifts.
|
||||
// Future shifts beyond 30 days should be created by a scheduled job.
|
||||
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
|
||||
final DateTime effectiveEndDate = order.endDate.isAfter(maxEndDate)
|
||||
? maxEndDate
|
||||
: order.endDate;
|
||||
|
||||
final Set<String> selectedDays = Set<String>.from(order.recurringDays);
|
||||
final int workersNeeded = order.positions.fold<int>(
|
||||
0,
|
||||
(int sum, domain.RecurringOrderPosition position) =>
|
||||
sum + position.count,
|
||||
);
|
||||
final double shiftCost = _calculateRecurringShiftCost(order);
|
||||
|
||||
final List<String> shiftIds = <String>[];
|
||||
for (
|
||||
DateTime day = orderDateOnly;
|
||||
!day.isAfter(effectiveEndDate);
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
final String dayLabel = _weekdayLabel(day);
|
||||
if (!selectedDays.contains(dayLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||
DateTime(day.year, day.month, day.day),
|
||||
);
|
||||
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(dayTimestamp)
|
||||
.location(hub.name)
|
||||
.locationAddress(hub.address)
|
||||
.latitude(hub.latitude)
|
||||
.longitude(hub.longitude)
|
||||
.placeId(hub.placeId)
|
||||
.city(hub.city)
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
.cost(shiftCost)
|
||||
.execute();
|
||||
|
||||
final String shiftId = shiftResult.data.shift_insert.id;
|
||||
shiftIds.add(shiftId);
|
||||
|
||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(day, position.startTime);
|
||||
final DateTime end = _parseTime(day, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
|
||||
await _service.connector
|
||||
.createShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: position.role,
|
||||
count: position.count,
|
||||
)
|
||||
.startTime(_service.toTimestamp(start))
|
||||
.endTime(_service.toTimestamp(normalizedEnd))
|
||||
.hours(hours)
|
||||
.breakType(_breakDurationFromValue(position.lunchBreak))
|
||||
.isBreakPaid(_isBreakPaid(position.lunchBreak))
|
||||
.totalValue(totalValue)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(AnyValue(shiftIds))
|
||||
.execute();
|
||||
});
|
||||
Future<void> createRecurringOrder(Map<String, dynamic> payload) async {
|
||||
await _api.post(ClientEndpoints.ordersRecurring, data: payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createPermanentOrder(domain.PermanentOrder order) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final String? vendorId = order.vendorId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
throw Exception('Vendor is missing.');
|
||||
}
|
||||
final domain.OneTimeOrderHubDetails? hub = order.hub;
|
||||
if (hub == null || hub.id.isEmpty) {
|
||||
throw Exception('Hub is missing.');
|
||||
}
|
||||
|
||||
final DateTime orderDateOnly = DateTime(
|
||||
order.startDate.year,
|
||||
order.startDate.month,
|
||||
order.startDate.day,
|
||||
);
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.PERMANENT,
|
||||
teamHubId: hub.id,
|
||||
)
|
||||
.vendorId(vendorId)
|
||||
.eventName(order.eventName)
|
||||
.status(dc.OrderStatus.POSTED)
|
||||
.date(orderTimestamp)
|
||||
.startDate(startTimestamp)
|
||||
.permanentDays(order.permanentDays)
|
||||
.execute();
|
||||
|
||||
final String orderId = orderResult.data.order_insert.id;
|
||||
|
||||
// NOTE: Permanent orders are limited to 30 days of generated shifts.
|
||||
// Future shifts beyond 30 days should be created by a scheduled job.
|
||||
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
|
||||
|
||||
final Set<String> selectedDays = Set<String>.from(order.permanentDays);
|
||||
final int workersNeeded = order.positions.fold<int>(
|
||||
0,
|
||||
(int sum, domain.OneTimeOrderPosition position) => sum + position.count,
|
||||
);
|
||||
final double shiftCost = _calculatePermanentShiftCost(order);
|
||||
|
||||
final List<String> shiftIds = <String>[];
|
||||
for (
|
||||
DateTime day = orderDateOnly;
|
||||
!day.isAfter(maxEndDate);
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
final String dayLabel = _weekdayLabel(day);
|
||||
if (!selectedDays.contains(dayLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||
DateTime(day.year, day.month, day.day),
|
||||
);
|
||||
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(dayTimestamp)
|
||||
.location(hub.name)
|
||||
.locationAddress(hub.address)
|
||||
.latitude(hub.latitude)
|
||||
.longitude(hub.longitude)
|
||||
.placeId(hub.placeId)
|
||||
.city(hub.city)
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
.cost(shiftCost)
|
||||
.execute();
|
||||
|
||||
final String shiftId = shiftResult.data.shift_insert.id;
|
||||
shiftIds.add(shiftId);
|
||||
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(day, position.startTime);
|
||||
final DateTime end = _parseTime(day, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
|
||||
await _service.connector
|
||||
.createShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: position.role,
|
||||
count: position.count,
|
||||
)
|
||||
.startTime(_service.toTimestamp(start))
|
||||
.endTime(_service.toTimestamp(normalizedEnd))
|
||||
.hours(hours)
|
||||
.breakType(_breakDurationFromValue(position.lunchBreak))
|
||||
.isBreakPaid(_isBreakPaid(position.lunchBreak))
|
||||
.totalValue(totalValue)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(AnyValue(shiftIds))
|
||||
.execute();
|
||||
});
|
||||
Future<void> createPermanentOrder(Map<String, dynamic> payload) async {
|
||||
await _api.post(ClientEndpoints.ordersPermanent, data: payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createRapidOrder(String description) async {
|
||||
// TO-DO: connect IA and return array with the information.
|
||||
throw UnimplementedError('Rapid order IA is not connected yet.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.OneTimeOrder> parseRapidOrder(String text) async {
|
||||
final RapidOrderParseResponse response = await _rapidOrderService.parseText(
|
||||
text: text,
|
||||
);
|
||||
final RapidOrderParsedData data = response.parsed;
|
||||
|
||||
// Fetch Business ID
|
||||
final String businessId = await _service.getBusinessId();
|
||||
|
||||
// 1. Hub Matching
|
||||
final OperationResult<
|
||||
dc.ListTeamHubsByOwnerIdData,
|
||||
dc.ListTeamHubsByOwnerIdVariables
|
||||
>
|
||||
hubResult = await _service.connector
|
||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
final List<dc.ListTeamHubsByOwnerIdTeamHubs> hubs = hubResult.data.teamHubs;
|
||||
|
||||
final dc.ListTeamHubsByOwnerIdTeamHubs? bestHub = _findBestHub(
|
||||
hubs,
|
||||
data.locationHint,
|
||||
);
|
||||
|
||||
// 2. Roles Matching
|
||||
// We fetch vendors to get the first one as a context for role matching.
|
||||
final OperationResult<dc.ListVendorsData, void> vendorResult =
|
||||
await _service.connector.listVendors().execute();
|
||||
final List<dc.ListVendorsVendors> vendors = vendorResult.data.vendors;
|
||||
|
||||
String? selectedVendorId;
|
||||
List<dc.ListRolesByVendorIdRoles> availableRoles =
|
||||
<dc.ListRolesByVendorIdRoles>[];
|
||||
|
||||
if (vendors.isNotEmpty) {
|
||||
selectedVendorId = vendors.first.id;
|
||||
final OperationResult<
|
||||
dc.ListRolesByVendorIdData,
|
||||
dc.ListRolesByVendorIdVariables
|
||||
>
|
||||
roleResult = await _service.connector
|
||||
.listRolesByVendorId(vendorId: selectedVendorId)
|
||||
.execute();
|
||||
availableRoles = roleResult.data.roles;
|
||||
}
|
||||
|
||||
final DateTime startAt =
|
||||
DateTime.tryParse(data.startAt ?? '') ?? DateTime.now();
|
||||
final DateTime endAt =
|
||||
DateTime.tryParse(data.endAt ?? '') ??
|
||||
startAt.add(const Duration(hours: 8));
|
||||
|
||||
final String startTimeStr = DateFormat('hh:mm a').format(startAt.toLocal());
|
||||
final String endTimeStr = DateFormat('hh:mm a').format(endAt.toLocal());
|
||||
|
||||
return domain.OneTimeOrder(
|
||||
date: startAt,
|
||||
location: bestHub?.hubName ?? data.locationHint ?? '',
|
||||
eventName: data.notes ?? '',
|
||||
vendorId: selectedVendorId,
|
||||
hub: bestHub != null
|
||||
? domain.OneTimeOrderHubDetails(
|
||||
id: bestHub.id,
|
||||
name: bestHub.hubName,
|
||||
address: bestHub.address,
|
||||
placeId: bestHub.placeId,
|
||||
latitude: bestHub.latitude ?? 0,
|
||||
longitude: bestHub.longitude ?? 0,
|
||||
city: bestHub.city,
|
||||
state: bestHub.state,
|
||||
street: bestHub.street,
|
||||
country: bestHub.country,
|
||||
zipCode: bestHub.zipCode,
|
||||
)
|
||||
: null,
|
||||
positions: data.positions.map((RapidOrderPosition p) {
|
||||
final dc.ListRolesByVendorIdRoles? matchedRole = _findBestRole(
|
||||
availableRoles,
|
||||
p.role,
|
||||
);
|
||||
return domain.OneTimeOrderPosition(
|
||||
role: matchedRole?.id ?? p.role,
|
||||
count: p.count,
|
||||
startTime: startTimeStr,
|
||||
endTime: endTimeStr,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
throw UnimplementedError('Rapid order creation is not connected yet.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> transcribeRapidOrder(String audioPath) async {
|
||||
// 1. Upload the audio file first
|
||||
final String fileName = audioPath.split('/').last;
|
||||
final FileUploadResponse uploadResponse = await _fileUploadService
|
||||
.uploadFile(
|
||||
filePath: audioPath,
|
||||
fileName: fileName,
|
||||
category: 'rapid-order-audio',
|
||||
);
|
||||
final FileUploadResponse uploadResponse =
|
||||
await _fileUploadService.uploadFile(
|
||||
filePath: audioPath,
|
||||
fileName: fileName,
|
||||
category: 'rapid-order-audio',
|
||||
);
|
||||
|
||||
// 2. Transcribe using the remote URI
|
||||
final RapidOrderTranscriptionResponse response = await _rapidOrderService
|
||||
.transcribeAudio(audioFileUri: uploadResponse.fileUri);
|
||||
final RapidOrderTranscriptionResponse response =
|
||||
await _rapidOrderService.transcribeAudio(
|
||||
audioFileUri: uploadResponse.fileUri,
|
||||
);
|
||||
return response.transcript;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reorder(String previousOrderId, DateTime newDate) async {
|
||||
// TODO: Implement reorder functionality to fetch the previous order and create a new one with the updated date.
|
||||
throw UnimplementedError('Reorder functionality is not yet implemented.');
|
||||
Future<Map<String, dynamic>> parseRapidOrder(String text) async {
|
||||
final RapidOrderParseResponse response =
|
||||
await _rapidOrderService.parseText(text: text);
|
||||
final RapidOrderParsedData data = response.parsed;
|
||||
|
||||
return <String, dynamic>{
|
||||
'eventName': data.notes ?? '',
|
||||
'locationHint': data.locationHint ?? '',
|
||||
'startAt': data.startAt,
|
||||
'endAt': data.endAt,
|
||||
'positions': data.positions
|
||||
.map((RapidOrderPosition p) => <String, dynamic>{
|
||||
'roleName': p.role,
|
||||
'workerCount': p.count,
|
||||
})
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ReorderData> getOrderDetailsForReorder(String orderId) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndOrderData,
|
||||
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndOrder(
|
||||
businessId: businessId,
|
||||
orderId: orderId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByBusinessAndOrderShiftRoles> shiftRoles =
|
||||
result.data.shiftRoles;
|
||||
|
||||
if (shiftRoles.isEmpty) {
|
||||
throw Exception('Order not found or has no roles.');
|
||||
}
|
||||
|
||||
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrder order =
|
||||
shiftRoles.first.shift.order;
|
||||
|
||||
final domain.OrderType orderType = _mapOrderType(order.orderType);
|
||||
|
||||
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub
|
||||
teamHub = order.teamHub;
|
||||
|
||||
return domain.ReorderData(
|
||||
orderId: orderId,
|
||||
eventName: order.eventName ?? '',
|
||||
vendorId: order.vendorId ?? '',
|
||||
orderType: orderType,
|
||||
hub: domain.OneTimeOrderHubDetails(
|
||||
id: teamHub.id,
|
||||
name: teamHub.hubName,
|
||||
address: teamHub.address,
|
||||
placeId: teamHub.placeId,
|
||||
latitude: 0, // Not available in this query
|
||||
longitude: 0,
|
||||
),
|
||||
positions: shiftRoles.map((
|
||||
dc.ListShiftRolesByBusinessAndOrderShiftRoles role,
|
||||
) {
|
||||
return domain.ReorderPosition(
|
||||
roleId: role.roleId,
|
||||
count: role.count,
|
||||
startTime: _formatTimestamp(role.startTime),
|
||||
endTime: _formatTimestamp(role.endTime),
|
||||
lunchBreak: _formatBreakDuration(role.breakType),
|
||||
);
|
||||
}).toList(),
|
||||
startDate: order.startDate?.toDateTime(),
|
||||
endDate: order.endDate?.toDateTime(),
|
||||
recurringDays: order.recurringDays ?? const <String>[],
|
||||
permanentDays: order.permanentDays ?? const <String>[],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
double _calculateShiftCost(domain.OneTimeOrder order) {
|
||||
double total = 0;
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.date, position.startTime);
|
||||
final DateTime end = _parseTime(order.date, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double _calculateRecurringShiftCost(domain.RecurringOrder order) {
|
||||
double total = 0;
|
||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double _calculatePermanentShiftCost(domain.PermanentOrder order) {
|
||||
double total = 0;
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
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:
|
||||
default:
|
||||
return 'SUN';
|
||||
}
|
||||
}
|
||||
|
||||
dc.BreakDuration _breakDurationFromValue(String value) {
|
||||
switch (value) {
|
||||
case 'MIN_10':
|
||||
return dc.BreakDuration.MIN_10;
|
||||
case 'MIN_15':
|
||||
return dc.BreakDuration.MIN_15;
|
||||
case 'MIN_30':
|
||||
return dc.BreakDuration.MIN_30;
|
||||
case 'MIN_45':
|
||||
return dc.BreakDuration.MIN_45;
|
||||
case 'MIN_60':
|
||||
return dc.BreakDuration.MIN_60;
|
||||
default:
|
||||
return dc.BreakDuration.NO_BREAK;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isBreakPaid(String value) {
|
||||
return value == 'MIN_10' || value == 'MIN_15';
|
||||
}
|
||||
|
||||
DateTime _parseTime(DateTime date, String time) {
|
||||
if (time.trim().isEmpty) {
|
||||
throw Exception('Shift time is missing.');
|
||||
}
|
||||
|
||||
DateTime parsed;
|
||||
try {
|
||||
parsed = DateFormat.jm().parse(time);
|
||||
} catch (_) {
|
||||
parsed = DateFormat.Hm().parse(time);
|
||||
}
|
||||
|
||||
return DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
parsed.hour,
|
||||
parsed.minute,
|
||||
Future<OrderPreview> getOrderDetailsForReorder(String orderId) async {
|
||||
final ApiResponse response = await _api.get(
|
||||
ClientEndpoints.orderReorderPreview(orderId),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime dateTime) {
|
||||
final String year = dateTime.year.toString().padLeft(4, '0');
|
||||
final String month = dateTime.month.toString().padLeft(2, '0');
|
||||
final String day = dateTime.day.toString().padLeft(2, '0');
|
||||
return '$year-$month-$day';
|
||||
}
|
||||
|
||||
String _formatTimestamp(Timestamp? value) {
|
||||
if (value == null) return '';
|
||||
try {
|
||||
return DateFormat('HH:mm').format(value.toDateTime());
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBreakDuration(dc.EnumValue<dc.BreakDuration>? breakType) {
|
||||
if (breakType is dc.Known<dc.BreakDuration>) {
|
||||
switch (breakType.value) {
|
||||
case dc.BreakDuration.MIN_10:
|
||||
return 'MIN_10';
|
||||
case dc.BreakDuration.MIN_15:
|
||||
return 'MIN_15';
|
||||
case dc.BreakDuration.MIN_30:
|
||||
return 'MIN_30';
|
||||
case dc.BreakDuration.MIN_45:
|
||||
return 'MIN_45';
|
||||
case dc.BreakDuration.MIN_60:
|
||||
return 'MIN_60';
|
||||
case dc.BreakDuration.NO_BREAK:
|
||||
return 'NO_BREAK';
|
||||
}
|
||||
}
|
||||
return 'NO_BREAK';
|
||||
}
|
||||
|
||||
domain.OrderType _mapOrderType(dc.EnumValue<dc.OrderType>? orderType) {
|
||||
if (orderType is dc.Known<dc.OrderType>) {
|
||||
switch (orderType.value) {
|
||||
case dc.OrderType.ONE_TIME:
|
||||
return domain.OrderType.oneTime;
|
||||
case dc.OrderType.RECURRING:
|
||||
return domain.OrderType.recurring;
|
||||
case dc.OrderType.PERMANENT:
|
||||
return domain.OrderType.permanent;
|
||||
case dc.OrderType.RAPID:
|
||||
return domain.OrderType.oneTime;
|
||||
}
|
||||
}
|
||||
return domain.OrderType.oneTime;
|
||||
}
|
||||
|
||||
dc.ListTeamHubsByOwnerIdTeamHubs? _findBestHub(
|
||||
List<dc.ListTeamHubsByOwnerIdTeamHubs> hubs,
|
||||
String? hint,
|
||||
) {
|
||||
if (hint == null || hint.isEmpty || hubs.isEmpty) return null;
|
||||
final String normalizedHint = hint.toLowerCase();
|
||||
|
||||
dc.ListTeamHubsByOwnerIdTeamHubs? bestMatch;
|
||||
double highestScore = -1;
|
||||
|
||||
for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) {
|
||||
final String name = hub.hubName.toLowerCase();
|
||||
final String address = hub.address.toLowerCase();
|
||||
|
||||
double score = 0;
|
||||
if (name == normalizedHint || address == normalizedHint) {
|
||||
score = 100;
|
||||
} else if (name.contains(normalizedHint) ||
|
||||
address.contains(normalizedHint)) {
|
||||
score = 80;
|
||||
} else if (normalizedHint.contains(name) ||
|
||||
normalizedHint.contains(address)) {
|
||||
score = 60;
|
||||
} else {
|
||||
final List<String> hintWords = normalizedHint.split(RegExp(r'\s+'));
|
||||
final List<String> hubWords = ('$name $address').split(RegExp(r'\s+'));
|
||||
int overlap = 0;
|
||||
for (final String word in hintWords) {
|
||||
if (word.length > 2 && hubWords.contains(word)) overlap++;
|
||||
}
|
||||
score = overlap * 10.0;
|
||||
}
|
||||
|
||||
if (score > highestScore) {
|
||||
highestScore = score;
|
||||
bestMatch = hub;
|
||||
}
|
||||
}
|
||||
|
||||
return (highestScore >= 10) ? bestMatch : null;
|
||||
}
|
||||
|
||||
dc.ListRolesByVendorIdRoles? _findBestRole(
|
||||
List<dc.ListRolesByVendorIdRoles> roles,
|
||||
String? hint,
|
||||
) {
|
||||
if (hint == null || hint.isEmpty || roles.isEmpty) return null;
|
||||
final String normalizedHint = hint.toLowerCase();
|
||||
|
||||
dc.ListRolesByVendorIdRoles? bestMatch;
|
||||
double highestScore = -1;
|
||||
|
||||
for (final dc.ListRolesByVendorIdRoles role in roles) {
|
||||
final String name = role.name.toLowerCase();
|
||||
|
||||
double score = 0;
|
||||
if (name == normalizedHint) {
|
||||
score = 100;
|
||||
} else if (name.contains(normalizedHint)) {
|
||||
score = 80;
|
||||
} else if (normalizedHint.contains(name)) {
|
||||
score = 60;
|
||||
} else {
|
||||
final List<String> hintWords = normalizedHint.split(RegExp(r'\s+'));
|
||||
final List<String> roleWords = name.split(RegExp(r'\s+'));
|
||||
int overlap = 0;
|
||||
for (final String word in hintWords) {
|
||||
if (word.length > 2 && roleWords.contains(word)) overlap++;
|
||||
}
|
||||
score = overlap * 10.0;
|
||||
}
|
||||
|
||||
if (score > highestScore) {
|
||||
highestScore = score;
|
||||
bestMatch = role;
|
||||
}
|
||||
}
|
||||
|
||||
return (highestScore >= 10) ? bestMatch : null;
|
||||
return OrderPreview.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/models/order_hub.dart';
|
||||
@@ -6,102 +6,83 @@ import '../../domain/models/order_manager.dart';
|
||||
import '../../domain/models/order_role.dart';
|
||||
import '../../domain/repositories/client_order_query_repository_interface.dart';
|
||||
|
||||
/// Data layer implementation of [ClientOrderQueryRepositoryInterface].
|
||||
/// V2 API implementation of [ClientOrderQueryRepositoryInterface].
|
||||
///
|
||||
/// Delegates all backend calls to [dc.DataConnectService] using the
|
||||
/// `_service.run()` pattern for automatic auth validation, token refresh,
|
||||
/// and retry logic. Each method maps Data Connect response types to the
|
||||
/// corresponding clean domain models.
|
||||
/// Delegates all backend calls to [BaseApiService] with [ClientEndpoints].
|
||||
class ClientOrderQueryRepositoryImpl
|
||||
implements ClientOrderQueryRepositoryInterface {
|
||||
/// Creates an instance backed by the given [service].
|
||||
ClientOrderQueryRepositoryImpl({required dc.DataConnectService service})
|
||||
: _service = service;
|
||||
/// Creates an instance backed by the given [apiService].
|
||||
ClientOrderQueryRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<Vendor>> getVendors() async {
|
||||
return _service.run(() async {
|
||||
final result = await _service.connector.listVendors().execute();
|
||||
return result.data.vendors
|
||||
.map(
|
||||
(dc.ListVendorsVendors vendor) => Vendor(
|
||||
id: vendor.id,
|
||||
name: vendor.companyName,
|
||||
rates: const <String, double>{},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
final ApiResponse response = await _api.get(ClientEndpoints.vendors);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) => Vendor.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OrderRole>> getRolesByVendor(String vendorId) async {
|
||||
return _service.run(() async {
|
||||
final result = await _service.connector
|
||||
.listRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
return result.data.roles
|
||||
.map(
|
||||
(dc.ListRolesByVendorIdRoles role) => OrderRole(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
final ApiResponse response =
|
||||
await _api.get(ClientEndpoints.vendorRoles(vendorId));
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.map((dynamic json) {
|
||||
final Map<String, dynamic> role = json as Map<String, dynamic>;
|
||||
return OrderRole(
|
||||
id: role['roleId'] as String? ?? role['id'] as String? ?? '',
|
||||
name: role['roleName'] as String? ?? role['name'] as String? ?? '',
|
||||
costPerHour:
|
||||
((role['hourlyRateCents'] as num?)?.toDouble() ?? 0) / 100.0,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OrderHub>> getHubsByOwner(String ownerId) async {
|
||||
return _service.run(() async {
|
||||
final result = await _service.connector
|
||||
.listTeamHubsByOwnerId(ownerId: ownerId)
|
||||
.execute();
|
||||
return result.data.teamHubs
|
||||
.map(
|
||||
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => OrderHub(
|
||||
id: hub.id,
|
||||
name: hub.hubName,
|
||||
address: hub.address,
|
||||
placeId: hub.placeId,
|
||||
latitude: hub.latitude,
|
||||
longitude: hub.longitude,
|
||||
city: hub.city,
|
||||
state: hub.state,
|
||||
street: hub.street,
|
||||
country: hub.country,
|
||||
zipCode: hub.zipCode,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
Future<List<OrderHub>> getHubs() async {
|
||||
final ApiResponse response = await _api.get(ClientEndpoints.hubs);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.map((dynamic json) {
|
||||
final Map<String, dynamic> hub = json as Map<String, dynamic>;
|
||||
return OrderHub(
|
||||
id: hub['hubId'] as String? ?? hub['id'] as String? ?? '',
|
||||
name: hub['hubName'] as String? ?? hub['name'] as String? ?? '',
|
||||
address:
|
||||
hub['fullAddress'] as String? ?? hub['address'] as String? ?? '',
|
||||
placeId: hub['placeId'] as String?,
|
||||
latitude: (hub['latitude'] as num?)?.toDouble(),
|
||||
longitude: (hub['longitude'] as num?)?.toDouble(),
|
||||
city: hub['city'] as String?,
|
||||
state: hub['state'] as String?,
|
||||
street: hub['street'] as String?,
|
||||
country: hub['country'] as String?,
|
||||
zipCode: hub['zipCode'] as String?,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OrderManager>> getManagersByHub(String hubId) async {
|
||||
return _service.run(() async {
|
||||
final result = await _service.connector.listTeamMembers().execute();
|
||||
return result.data.teamMembers
|
||||
.where(
|
||||
(dc.ListTeamMembersTeamMembers member) =>
|
||||
member.teamHubId == hubId &&
|
||||
member.role is dc.Known<dc.TeamMemberRole> &&
|
||||
(member.role as dc.Known<dc.TeamMemberRole>).value ==
|
||||
dc.TeamMemberRole.MANAGER,
|
||||
)
|
||||
.map(
|
||||
(dc.ListTeamMembersTeamMembers member) => OrderManager(
|
||||
id: member.id,
|
||||
name: member.user.fullName ?? 'Unknown',
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
final ApiResponse response =
|
||||
await _api.get(ClientEndpoints.hubManagers(hubId));
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.map((dynamic json) {
|
||||
final Map<String, dynamic> mgr = json as Map<String, dynamic>;
|
||||
return OrderManager(
|
||||
id: mgr['managerAssignmentId'] as String? ??
|
||||
mgr['businessMembershipId'] as String? ??
|
||||
mgr['id'] as String? ??
|
||||
'',
|
||||
name: mgr['name'] as String? ?? '',
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getBusinessId() => _service.getBusinessId();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
|
||||
/// Arguments for the [CreateOneTimeOrderUseCase].
|
||||
///
|
||||
/// Encapsulates the [OneTimeOrder] details required to create a new
|
||||
/// one-time staffing request.
|
||||
/// Wraps the V2 API payload map for a one-time order.
|
||||
class OneTimeOrderArguments extends UseCaseArgument {
|
||||
/// Creates a [OneTimeOrderArguments] instance.
|
||||
///
|
||||
/// Requires the [order] details.
|
||||
const OneTimeOrderArguments({required this.order});
|
||||
/// Creates a [OneTimeOrderArguments] with the given [payload].
|
||||
const OneTimeOrderArguments({required this.payload});
|
||||
|
||||
/// The order details to be created.
|
||||
final OneTimeOrder order;
|
||||
/// The V2 API payload map.
|
||||
final Map<String, dynamic> payload;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[order];
|
||||
List<Object?> get props => <Object?>[payload];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Arguments for the [CreatePermanentOrderUseCase].
|
||||
///
|
||||
/// Wraps the V2 API payload map for a permanent order.
|
||||
class PermanentOrderArguments {
|
||||
const PermanentOrderArguments({required this.order});
|
||||
final PermanentOrder order;
|
||||
/// Creates a [PermanentOrderArguments] with the given [payload].
|
||||
const PermanentOrderArguments({required this.payload});
|
||||
|
||||
/// The V2 API payload map.
|
||||
final Map<String, dynamic> payload;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Arguments for the [CreateRecurringOrderUseCase].
|
||||
///
|
||||
/// Wraps the V2 API payload map for a recurring order.
|
||||
class RecurringOrderArguments {
|
||||
const RecurringOrderArguments({required this.order});
|
||||
final RecurringOrder order;
|
||||
/// Creates a [RecurringOrderArguments] with the given [payload].
|
||||
const RecurringOrderArguments({required this.payload});
|
||||
|
||||
/// The V2 API payload map.
|
||||
final Map<String, dynamic> payload;
|
||||
}
|
||||
|
||||
@@ -2,46 +2,33 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the Client Create Order repository.
|
||||
///
|
||||
/// This repository is responsible for:
|
||||
/// 1. Submitting different types of staffing orders (Rapid, One-Time, Recurring, Permanent).
|
||||
///
|
||||
/// It follows the KROW Clean Architecture by defining the contract in the
|
||||
/// domain layer, to be implemented in the data layer.
|
||||
/// V2 API uses typed endpoints per order type. Each method receives
|
||||
/// a [Map<String, dynamic>] payload matching the V2 schema.
|
||||
abstract interface class ClientCreateOrderRepositoryInterface {
|
||||
/// Submits a one-time staffing order with specific details.
|
||||
/// Submits a one-time staffing order.
|
||||
///
|
||||
/// [order] contains the date, location, and required positions.
|
||||
Future<void> createOneTimeOrder(OneTimeOrder order);
|
||||
/// [payload] follows the V2 `clientOneTimeOrderSchema` shape.
|
||||
Future<void> createOneTimeOrder(Map<String, dynamic> payload);
|
||||
|
||||
/// Submits a recurring staffing order with specific details.
|
||||
Future<void> createRecurringOrder(RecurringOrder order);
|
||||
/// Submits a recurring staffing order.
|
||||
///
|
||||
/// [payload] follows the V2 `clientRecurringOrderSchema` shape.
|
||||
Future<void> createRecurringOrder(Map<String, dynamic> payload);
|
||||
|
||||
/// Submits a permanent staffing order with specific details.
|
||||
Future<void> createPermanentOrder(PermanentOrder order);
|
||||
/// Submits a permanent staffing order.
|
||||
///
|
||||
/// [payload] follows the V2 `clientPermanentOrderSchema` shape.
|
||||
Future<void> createPermanentOrder(Map<String, dynamic> payload);
|
||||
|
||||
/// Submits a rapid (urgent) staffing order via a text description.
|
||||
///
|
||||
/// [description] is the text message (or transcribed voice) describing the need.
|
||||
Future<void> createRapidOrder(String description);
|
||||
|
||||
/// Transcribes the audio file for a rapid order.
|
||||
///
|
||||
/// [audioPath] is the local path to the recorded audio file.
|
||||
Future<String> transcribeRapidOrder(String audioPath);
|
||||
|
||||
/// Parses the text description for a rapid order into a structured draft.
|
||||
///
|
||||
/// [text] is the text message describing the need.
|
||||
Future<OneTimeOrder> parseRapidOrder(String text);
|
||||
/// Parses the text description into a structured draft payload.
|
||||
Future<Map<String, dynamic>> parseRapidOrder(String text);
|
||||
|
||||
/// Reorders an existing staffing order with a new date.
|
||||
///
|
||||
/// [previousOrderId] is the ID of the order to reorder.
|
||||
/// [newDate] is the new date for the order.
|
||||
Future<void> reorder(String previousOrderId, DateTime newDate);
|
||||
|
||||
/// Fetches the details of an existing order to be used as a template for a new order.
|
||||
///
|
||||
/// returns [ReorderData] containing the order details and positions.
|
||||
Future<ReorderData> getOrderDetailsForReorder(String orderId);
|
||||
/// Fetches the reorder preview for an existing order.
|
||||
Future<OrderPreview> getOrderDetailsForReorder(String orderId);
|
||||
}
|
||||
|
||||
@@ -6,34 +6,19 @@ import '../models/order_role.dart';
|
||||
|
||||
/// Interface for querying order-related reference data.
|
||||
///
|
||||
/// This repository centralises the read-only queries that the order creation
|
||||
/// BLoCs need (vendors, roles, hubs, managers) so that they no longer depend
|
||||
/// directly on [DataConnectService] or the `krow_data_connect` package.
|
||||
///
|
||||
/// Implementations live in the data layer and translate backend responses
|
||||
/// into clean domain models.
|
||||
/// Implementations use V2 API endpoints for vendors, roles, hubs, and
|
||||
/// managers. The V2 API resolves the business context from the auth token,
|
||||
/// so no explicit business ID parameter is needed.
|
||||
abstract interface class ClientOrderQueryRepositoryInterface {
|
||||
/// Returns the list of available vendors.
|
||||
///
|
||||
/// The returned [Vendor] objects come from the shared `krow_domain` package
|
||||
/// because `Vendor` is already a clean domain entity.
|
||||
Future<List<Vendor>> getVendors();
|
||||
|
||||
/// Returns the roles offered by the vendor identified by [vendorId].
|
||||
Future<List<OrderRole>> getRolesByVendor(String vendorId);
|
||||
|
||||
/// Returns the team hubs owned by the business identified by [ownerId].
|
||||
Future<List<OrderHub>> getHubsByOwner(String ownerId);
|
||||
/// Returns the hubs for the current business.
|
||||
Future<List<OrderHub>> getHubs();
|
||||
|
||||
/// Returns the managers assigned to the hub identified by [hubId].
|
||||
///
|
||||
/// Only team members with the MANAGER role at the given hub are included.
|
||||
Future<List<OrderManager>> getManagersByHub(String hubId);
|
||||
|
||||
/// Returns the current business ID from the active client session.
|
||||
///
|
||||
/// This allows BLoCs to resolve the business ID without depending on
|
||||
/// the data layer's session store directly, keeping the presentation
|
||||
/// layer free from `krow_data_connect` imports.
|
||||
Future<String> getBusinessId();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
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.
|
||||
///
|
||||
/// This use case encapsulates the logic for submitting a structured
|
||||
/// staffing request and delegates the data operation to the
|
||||
/// [ClientCreateOrderRepositoryInterface].
|
||||
/// Delegates the V2 API payload to the repository.
|
||||
class CreateOneTimeOrderUseCase
|
||||
implements UseCase<OneTimeOrderArguments, void> {
|
||||
/// Creates a [CreateOneTimeOrderUseCase].
|
||||
///
|
||||
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
|
||||
const CreateOneTimeOrderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(OneTimeOrderArguments input) {
|
||||
return _repository.createOneTimeOrder(input.order);
|
||||
return _repository.createOneTimeOrder(input.payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../arguments/permanent_order_arguments.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for creating a permanent staffing order.
|
||||
class CreatePermanentOrderUseCase implements UseCase<PermanentOrder, void> {
|
||||
///
|
||||
/// Delegates the V2 API payload to the repository.
|
||||
class CreatePermanentOrderUseCase {
|
||||
/// Creates a [CreatePermanentOrderUseCase].
|
||||
const CreatePermanentOrderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(PermanentOrder params) {
|
||||
return _repository.createPermanentOrder(params);
|
||||
/// Executes the use case with the given [args].
|
||||
Future<void> call(PermanentOrderArguments args) {
|
||||
return _repository.createPermanentOrder(args.payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../arguments/recurring_order_arguments.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for creating a recurring staffing order.
|
||||
class CreateRecurringOrderUseCase implements UseCase<RecurringOrder, void> {
|
||||
///
|
||||
/// Delegates the V2 API payload to the repository.
|
||||
class CreateRecurringOrderUseCase {
|
||||
/// Creates a [CreateRecurringOrderUseCase].
|
||||
const CreateRecurringOrderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(RecurringOrder params) {
|
||||
return _repository.createRecurringOrder(params);
|
||||
/// Executes the use case with the given [args].
|
||||
Future<void> call(RecurringOrderArguments args) {
|
||||
return _repository.createRecurringOrder(args.payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching order details for reordering.
|
||||
class GetOrderDetailsForReorderUseCase implements UseCase<String, ReorderData> {
|
||||
///
|
||||
/// Returns an [OrderPreview] from the V2 reorder-preview endpoint.
|
||||
class GetOrderDetailsForReorderUseCase
|
||||
implements UseCase<String, OrderPreview> {
|
||||
/// Creates a [GetOrderDetailsForReorderUseCase].
|
||||
const GetOrderDetailsForReorderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<ReorderData> call(String orderId) {
|
||||
Future<OrderPreview> call(String orderId) {
|
||||
return _repository.getOrderDetailsForReorder(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for parsing rapid order text into a structured OneTimeOrder.
|
||||
/// Use case for parsing rapid order text into a structured draft.
|
||||
///
|
||||
/// Returns a [Map<String, dynamic>] containing parsed order data.
|
||||
class ParseRapidOrderTextToOrderUseCase {
|
||||
/// Creates a [ParseRapidOrderTextToOrderUseCase].
|
||||
ParseRapidOrderTextToOrderUseCase({
|
||||
required ClientCreateOrderRepositoryInterface repository,
|
||||
}) : _repository = repository;
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
Future<OneTimeOrder> call(String text) async {
|
||||
/// Parses the given [text] into an order draft map.
|
||||
Future<Map<String, dynamic>> call(String text) async {
|
||||
return _repository.parseRapidOrder(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Arguments for the ReorderUseCase.
|
||||
class ReorderArguments {
|
||||
const ReorderArguments({
|
||||
required this.previousOrderId,
|
||||
required this.newDate,
|
||||
});
|
||||
|
||||
final String previousOrderId;
|
||||
final DateTime newDate;
|
||||
}
|
||||
|
||||
/// Use case for reordering an existing staffing order.
|
||||
class ReorderUseCase implements UseCase<ReorderArguments, void> {
|
||||
const ReorderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(ReorderArguments params) {
|
||||
return _repository.reorder(params.previousOrderId, params.newDate);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,13 @@ import 'one_time_order_event.dart';
|
||||
import 'one_time_order_state.dart';
|
||||
|
||||
/// BLoC for managing the multi-step one-time order creation form.
|
||||
///
|
||||
/// Builds V2 API payloads and uses [OrderPreview] for reorder.
|
||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
with
|
||||
BlocErrorHandler<OneTimeOrderState>,
|
||||
SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
/// Creates the BLoC with required dependencies.
|
||||
OneTimeOrderBloc(
|
||||
this._createOneTimeOrderUseCase,
|
||||
this._getOrderDetailsForReorderUseCase,
|
||||
@@ -39,6 +42,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
_loadVendors();
|
||||
_loadHubs();
|
||||
}
|
||||
|
||||
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
|
||||
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||
final ClientOrderQueryRepositoryInterface _queryRepository;
|
||||
@@ -48,10 +52,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
action: () => _queryRepository.getVendors(),
|
||||
onError: (_) => add(const OneTimeOrderVendorsLoaded(<Vendor>[])),
|
||||
);
|
||||
|
||||
if (vendors != null) {
|
||||
add(OneTimeOrderVendorsLoaded(vendors));
|
||||
}
|
||||
if (vendors != null) add(OneTimeOrderVendorsLoaded(vendors));
|
||||
}
|
||||
|
||||
Future<void> _loadRolesForVendor(
|
||||
@@ -63,98 +64,70 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
final List<OrderRole> result =
|
||||
await _queryRepository.getRolesByVendor(vendorId);
|
||||
return result
|
||||
.map(
|
||||
(OrderRole r) => OneTimeOrderRoleOption(
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
costPerHour: r.costPerHour,
|
||||
),
|
||||
)
|
||||
.map((OrderRole r) => OneTimeOrderRoleOption(
|
||||
id: r.id, name: r.name, costPerHour: r.costPerHour))
|
||||
.toList();
|
||||
},
|
||||
onError: (_) =>
|
||||
emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
|
||||
);
|
||||
|
||||
if (roles != null) {
|
||||
emit(state.copyWith(roles: roles));
|
||||
}
|
||||
if (roles != null) emit(state.copyWith(roles: roles));
|
||||
}
|
||||
|
||||
Future<void> _loadHubs() async {
|
||||
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String businessId = await _queryRepository.getBusinessId();
|
||||
final List<OrderHub> result =
|
||||
await _queryRepository.getHubsByOwner(businessId);
|
||||
final List<OrderHub> result = await _queryRepository.getHubs();
|
||||
return result
|
||||
.map(
|
||||
(OrderHub h) => OneTimeOrderHubOption(
|
||||
id: h.id,
|
||||
name: h.name,
|
||||
address: h.address,
|
||||
placeId: h.placeId,
|
||||
latitude: h.latitude,
|
||||
longitude: h.longitude,
|
||||
city: h.city,
|
||||
state: h.state,
|
||||
street: h.street,
|
||||
country: h.country,
|
||||
zipCode: h.zipCode,
|
||||
),
|
||||
)
|
||||
.map((OrderHub h) => OneTimeOrderHubOption(
|
||||
id: h.id,
|
||||
name: h.name,
|
||||
address: h.address,
|
||||
placeId: h.placeId,
|
||||
latitude: h.latitude,
|
||||
longitude: h.longitude,
|
||||
city: h.city,
|
||||
state: h.state,
|
||||
street: h.street,
|
||||
country: h.country,
|
||||
zipCode: h.zipCode,
|
||||
))
|
||||
.toList();
|
||||
},
|
||||
onError: (_) =>
|
||||
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
|
||||
);
|
||||
|
||||
if (hubs != null) {
|
||||
add(OneTimeOrderHubsLoaded(hubs));
|
||||
}
|
||||
if (hubs != null) add(OneTimeOrderHubsLoaded(hubs));
|
||||
}
|
||||
|
||||
Future<void> _loadManagersForHub(String hubId) async {
|
||||
final List<OneTimeOrderManagerOption>? managers =
|
||||
await handleErrorWithResult(
|
||||
action: () async {
|
||||
final List<OrderManager> result =
|
||||
await _queryRepository.getManagersByHub(hubId);
|
||||
return result
|
||||
.map(
|
||||
(OrderManager m) => OneTimeOrderManagerOption(
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) {
|
||||
add(
|
||||
const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (managers != null) {
|
||||
add(OneTimeOrderManagersLoaded(managers));
|
||||
}
|
||||
action: () async {
|
||||
final List<OrderManager> result =
|
||||
await _queryRepository.getManagersByHub(hubId);
|
||||
return result
|
||||
.map((OrderManager m) =>
|
||||
OneTimeOrderManagerOption(id: m.id, name: m.name))
|
||||
.toList();
|
||||
},
|
||||
onError: (_) =>
|
||||
add(const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[])),
|
||||
);
|
||||
if (managers != null) add(OneTimeOrderManagersLoaded(managers));
|
||||
}
|
||||
|
||||
Future<void> _onVendorsLoaded(
|
||||
OneTimeOrderVendorsLoaded event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) async {
|
||||
final Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||
? event.vendors.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
vendors: event.vendors,
|
||||
selectedVendor: selectedVendor,
|
||||
isDataLoaded: true,
|
||||
),
|
||||
);
|
||||
final Vendor? selectedVendor =
|
||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
||||
emit(state.copyWith(
|
||||
vendors: event.vendors,
|
||||
selectedVendor: selectedVendor,
|
||||
isDataLoaded: true,
|
||||
));
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
@@ -172,20 +145,14 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
OneTimeOrderHubsLoaded event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) {
|
||||
final OneTimeOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||
? event.hubs.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
hubs: event.hubs,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedHub != null) {
|
||||
_loadManagersForHub(selectedHub.id);
|
||||
}
|
||||
final OneTimeOrderHubOption? selectedHub =
|
||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
||||
emit(state.copyWith(
|
||||
hubs: event.hubs,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
));
|
||||
if (selectedHub != null) _loadManagersForHub(selectedHub.id);
|
||||
}
|
||||
|
||||
void _onHubChanged(
|
||||
@@ -229,14 +196,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) {
|
||||
final List<OneTimeOrderPosition> newPositions =
|
||||
List<OneTimeOrderPosition>.from(state.positions)..add(
|
||||
const OneTimeOrderPosition(
|
||||
role: '',
|
||||
count: 1,
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
),
|
||||
);
|
||||
List<OneTimeOrderPosition>.from(state.positions)
|
||||
..add(const OneTimeOrderPosition(
|
||||
role: '', count: 1, startTime: '09:00', endTime: '17:00'));
|
||||
emit(state.copyWith(positions: newPositions));
|
||||
}
|
||||
|
||||
@@ -262,6 +224,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
emit(state.copyWith(positions: newPositions));
|
||||
}
|
||||
|
||||
/// Builds a V2 API payload and submits the one-time order.
|
||||
Future<void> _onSubmitted(
|
||||
OneTimeOrderSubmitted event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
@@ -270,37 +233,45 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final Map<String, double> roleRates = <String, double>{
|
||||
for (final OneTimeOrderRoleOption role in state.roles)
|
||||
role.id: role.costPerHour,
|
||||
};
|
||||
final OneTimeOrderHubOption? selectedHub = state.selectedHub;
|
||||
if (selectedHub == null) {
|
||||
throw const OrderMissingHubException();
|
||||
}
|
||||
final OneTimeOrder order = OneTimeOrder(
|
||||
date: state.date,
|
||||
location: selectedHub.name,
|
||||
positions: state.positions,
|
||||
hub: OneTimeOrderHubDetails(
|
||||
id: selectedHub.id,
|
||||
name: selectedHub.name,
|
||||
address: selectedHub.address,
|
||||
placeId: selectedHub.placeId,
|
||||
latitude: selectedHub.latitude,
|
||||
longitude: selectedHub.longitude,
|
||||
city: selectedHub.city,
|
||||
state: selectedHub.state,
|
||||
street: selectedHub.street,
|
||||
country: selectedHub.country,
|
||||
zipCode: selectedHub.zipCode,
|
||||
),
|
||||
eventName: state.eventName,
|
||||
vendorId: state.selectedVendor?.id,
|
||||
hubManagerId: state.selectedManager?.id,
|
||||
roleRates: roleRates,
|
||||
if (selectedHub == null) throw const OrderMissingHubException();
|
||||
|
||||
final String orderDate =
|
||||
'${state.date.year.toString().padLeft(4, '0')}-'
|
||||
'${state.date.month.toString().padLeft(2, '0')}-'
|
||||
'${state.date.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final List<Map<String, dynamic>> positions =
|
||||
state.positions.map((OneTimeOrderPosition p) {
|
||||
final OneTimeOrderRoleOption? role = state.roles
|
||||
.cast<OneTimeOrderRoleOption?>()
|
||||
.firstWhere(
|
||||
(OneTimeOrderRoleOption? r) => r != null && r.id == p.role,
|
||||
orElse: () => null,
|
||||
);
|
||||
return <String, dynamic>{
|
||||
if (role != null) 'roleName': role.name,
|
||||
if (p.role.isNotEmpty) 'roleId': p.role,
|
||||
'workerCount': p.count,
|
||||
'startTime': p.startTime,
|
||||
'endTime': p.endTime,
|
||||
if (p.lunchBreak != 'NO_BREAK' && p.lunchBreak.isNotEmpty)
|
||||
'lunchBreakMinutes': _breakMinutes(p.lunchBreak),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final Map<String, dynamic> payload = <String, dynamic>{
|
||||
'hubId': selectedHub.id,
|
||||
'eventName': state.eventName,
|
||||
'orderDate': orderDate,
|
||||
'positions': positions,
|
||||
if (state.selectedVendor != null)
|
||||
'vendorId': state.selectedVendor!.id,
|
||||
};
|
||||
|
||||
await _createOneTimeOrderUseCase(
|
||||
OneTimeOrderArguments(payload: payload),
|
||||
);
|
||||
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
||||
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
@@ -310,6 +281,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Initializes the form from route arguments or reorder preview data.
|
||||
Future<void> _onInitialized(
|
||||
OneTimeOrderInitialized event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
@@ -321,39 +293,30 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
|
||||
// Handle Rapid Order Draft
|
||||
if (data['isRapidDraft'] == true) {
|
||||
final OneTimeOrder? order = data['order'] as OneTimeOrder?;
|
||||
if (order != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: order.eventName ?? '',
|
||||
date: order.date,
|
||||
positions: order.positions,
|
||||
location: order.location,
|
||||
isRapidDraft: true,
|
||||
),
|
||||
);
|
||||
final Map<String, dynamic>? draft =
|
||||
data['order'] as Map<String, dynamic>?;
|
||||
if (draft != null) {
|
||||
final List<dynamic> draftPositions =
|
||||
draft['positions'] as List<dynamic>? ?? const <dynamic>[];
|
||||
final List<OneTimeOrderPosition> positions = draftPositions
|
||||
.map((dynamic p) {
|
||||
final Map<String, dynamic> pos = p as Map<String, dynamic>;
|
||||
return OneTimeOrderPosition(
|
||||
role: pos['roleName'] as String? ?? '',
|
||||
count: pos['workerCount'] as int? ?? 1,
|
||||
startTime: pos['startTime'] as String? ?? '09:00',
|
||||
endTime: pos['endTime'] as String? ?? '17:00',
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Try to match vendor if available
|
||||
if (order.vendorId != null) {
|
||||
final Vendor? vendor = state.vendors
|
||||
.where((Vendor v) => v.id == order.vendorId)
|
||||
.firstOrNull;
|
||||
if (vendor != null) {
|
||||
emit(state.copyWith(selectedVendor: vendor));
|
||||
await _loadRolesForVendor(vendor.id, emit);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match hub if available
|
||||
if (order.hub != null) {
|
||||
final OneTimeOrderHubOption? hub = state.hubs
|
||||
.where((OneTimeOrderHubOption h) => h.id == order.hub?.id)
|
||||
.firstOrNull;
|
||||
if (hub != null) {
|
||||
emit(state.copyWith(selectedHub: hub));
|
||||
await _loadManagersForHub(hub.id);
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(
|
||||
eventName: draft['eventName'] as String? ?? '',
|
||||
date: startDate ?? DateTime.now(),
|
||||
positions: positions.isNotEmpty ? positions : null,
|
||||
location: draft['locationHint'] as String? ?? '',
|
||||
isRapidDraft: true,
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -367,50 +330,26 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final ReorderData orderDetails =
|
||||
final OrderPreview preview =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<OneTimeOrderPosition> positions = orderDetails.positions.map(
|
||||
(ReorderPosition role) {
|
||||
return OneTimeOrderPosition(
|
||||
final List<OneTimeOrderPosition> positions = <OneTimeOrderPosition>[];
|
||||
for (final OrderPreviewShift shift in preview.shifts) {
|
||||
for (final OrderPreviewRole role in shift.roles) {
|
||||
positions.add(OneTimeOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
// Update state with order details
|
||||
final Vendor? selectedVendor = state.vendors
|
||||
.where((Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final OneTimeOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(OneTimeOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
status: OneTimeOrderStatus.initial,
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
count: role.workersNeeded,
|
||||
startTime: _formatTime(shift.startsAt),
|
||||
endTime: _formatTime(shift.endsAt),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
eventName: preview.title.isNotEmpty ? preview.title : title,
|
||||
positions: positions.isNotEmpty ? positions : null,
|
||||
status: OneTimeOrderStatus.initial,
|
||||
));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: OneTimeOrderStatus.failure,
|
||||
@@ -418,4 +357,29 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] to HH:mm string.
|
||||
String _formatTime(DateTime dt) {
|
||||
final DateTime local = dt.toLocal();
|
||||
return '${local.hour.toString().padLeft(2, '0')}:'
|
||||
'${local.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// Converts a break duration string to minutes.
|
||||
int _breakMinutes(String value) {
|
||||
switch (value) {
|
||||
case 'MIN_10':
|
||||
return 10;
|
||||
case 'MIN_15':
|
||||
return 15;
|
||||
case 'MIN_30':
|
||||
return 30;
|
||||
case 'MIN_45':
|
||||
return 45;
|
||||
case 'MIN_60':
|
||||
return 60;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../utils/time_parsing_utils.dart';
|
||||
|
||||
/// Position type alias for one-time orders.
|
||||
typedef OneTimeOrderPosition = OrderPositionUiModel;
|
||||
|
||||
enum OneTimeOrderStatus { initial, loading, success, failure }
|
||||
|
||||
class OneTimeOrderState extends Equatable {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:client_create_order/src/domain/usecases/create_permanent_order_u
|
||||
import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:client_create_order/src/domain/arguments/permanent_order_arguments.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import 'permanent_order_event.dart';
|
||||
@@ -95,12 +96,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
Future<void> _loadHubs() async {
|
||||
final List<PermanentOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String? businessId = await _queryRepository.getBusinessId();
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return <PermanentOrderHubOption>[];
|
||||
}
|
||||
final List<OrderHub> orderHubs =
|
||||
await _queryRepository.getHubsByOwner(businessId);
|
||||
final List<OrderHub> orderHubs = await _queryRepository.getHubs();
|
||||
return orderHubs
|
||||
.map(
|
||||
(OrderHub hub) => PermanentOrderHubOption(
|
||||
@@ -327,48 +323,50 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final Map<String, double> roleRates = <String, double>{
|
||||
for (final PermanentOrderRoleOption role in state.roles)
|
||||
role.id: role.costPerHour,
|
||||
};
|
||||
final PermanentOrderHubOption? selectedHub = state.selectedHub;
|
||||
if (selectedHub == null) {
|
||||
throw const domain.OrderMissingHubException();
|
||||
}
|
||||
final domain.PermanentOrder order = domain.PermanentOrder(
|
||||
startDate: state.startDate,
|
||||
permanentDays: state.permanentDays,
|
||||
positions: state.positions
|
||||
.map(
|
||||
(PermanentOrderPosition p) => domain.OneTimeOrderPosition(
|
||||
role: p.role,
|
||||
count: p.count,
|
||||
startTime: p.startTime,
|
||||
endTime: p.endTime,
|
||||
lunchBreak: p.lunchBreak ?? 'NO_BREAK',
|
||||
location: null,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
hub: domain.OneTimeOrderHubDetails(
|
||||
id: selectedHub.id,
|
||||
name: selectedHub.name,
|
||||
address: selectedHub.address,
|
||||
placeId: selectedHub.placeId,
|
||||
latitude: selectedHub.latitude,
|
||||
longitude: selectedHub.longitude,
|
||||
city: selectedHub.city,
|
||||
state: selectedHub.state,
|
||||
street: selectedHub.street,
|
||||
country: selectedHub.country,
|
||||
zipCode: selectedHub.zipCode,
|
||||
),
|
||||
eventName: state.eventName,
|
||||
vendorId: state.selectedVendor?.id,
|
||||
hubManagerId: state.selectedManager?.id,
|
||||
roleRates: roleRates,
|
||||
|
||||
final String startDate =
|
||||
'${state.startDate.year.toString().padLeft(4, '0')}-'
|
||||
'${state.startDate.month.toString().padLeft(2, '0')}-'
|
||||
'${state.startDate.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final List<int> daysOfWeek = state.permanentDays
|
||||
.map((String day) => _dayLabels.indexOf(day) % 7)
|
||||
.toList();
|
||||
|
||||
final List<Map<String, dynamic>> positions =
|
||||
state.positions.map((PermanentOrderPosition p) {
|
||||
final PermanentOrderRoleOption? role = state.roles
|
||||
.cast<PermanentOrderRoleOption?>()
|
||||
.firstWhere(
|
||||
(PermanentOrderRoleOption? r) => r != null && r.id == p.role,
|
||||
orElse: () => null,
|
||||
);
|
||||
return <String, dynamic>{
|
||||
if (role != null) 'roleName': role.name,
|
||||
if (p.role.isNotEmpty) 'roleId': p.role,
|
||||
'workerCount': p.count,
|
||||
'startTime': p.startTime,
|
||||
'endTime': p.endTime,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final Map<String, dynamic> payload = <String, dynamic>{
|
||||
'hubId': selectedHub.id,
|
||||
'eventName': state.eventName,
|
||||
'startDate': startDate,
|
||||
'daysOfWeek': daysOfWeek,
|
||||
'positions': positions,
|
||||
if (state.selectedVendor != null)
|
||||
'vendorId': state.selectedVendor!.id,
|
||||
};
|
||||
|
||||
await _createPermanentOrderUseCase(
|
||||
PermanentOrderArguments(payload: payload),
|
||||
);
|
||||
await _createPermanentOrderUseCase(order);
|
||||
emit(state.copyWith(status: PermanentOrderStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
@@ -398,52 +396,32 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final domain.ReorderData orderDetails =
|
||||
final domain.OrderPreview preview =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<PermanentOrderPosition> positions = orderDetails.positions
|
||||
.map((domain.ReorderPosition role) {
|
||||
return PermanentOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Update state with order details
|
||||
final domain.Vendor? selectedVendor = state.vendors
|
||||
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final PermanentOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(PermanentOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
final List<PermanentOrderPosition> positions =
|
||||
<PermanentOrderPosition>[];
|
||||
for (final domain.OrderPreviewShift shift in preview.shifts) {
|
||||
for (final domain.OrderPreviewRole role in shift.roles) {
|
||||
positions.add(PermanentOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.workersNeeded,
|
||||
startTime: _formatTime(shift.startsAt),
|
||||
endTime: _formatTime(shift.endsAt),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
eventName:
|
||||
preview.title.isNotEmpty ? preview.title : title,
|
||||
positions: positions.isNotEmpty ? positions : null,
|
||||
status: PermanentOrderStatus.initial,
|
||||
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
|
||||
permanentDays: orderDetails.permanentDays,
|
||||
startDate:
|
||||
startDate ?? preview.startsAt ?? DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: PermanentOrderStatus.failure,
|
||||
@@ -452,6 +430,13 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] to HH:mm string.
|
||||
String _formatTime(DateTime dt) {
|
||||
final DateTime local = dt.toLocal();
|
||||
return '${local.hour.toString().padLeft(2, '0')}:'
|
||||
'${local.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
static List<String> _sortDays(List<String> days) {
|
||||
days.sort(
|
||||
(String a, String b) =>
|
||||
|
||||
@@ -2,8 +2,6 @@ import 'package:client_create_order/src/domain/usecases/parse_rapid_order_usecas
|
||||
import 'package:client_create_order/src/domain/usecases/transcribe_rapid_order_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'rapid_order_event.dart';
|
||||
import 'rapid_order_state.dart';
|
||||
|
||||
@@ -119,9 +117,13 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final OneTimeOrder order = await _parseRapidOrderUseCase(message);
|
||||
final Map<String, dynamic> parsedDraft =
|
||||
await _parseRapidOrderUseCase(message);
|
||||
emit(
|
||||
state.copyWith(status: RapidOrderStatus.parsed, parsedOrder: order),
|
||||
state.copyWith(
|
||||
status: RapidOrderStatus.parsed,
|
||||
parsedDraft: parsedDraft,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) =>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Status of the rapid order creation flow.
|
||||
enum RapidOrderStatus { initial, submitting, parsed, failure }
|
||||
|
||||
/// State for the rapid order BLoC.
|
||||
class RapidOrderState extends Equatable {
|
||||
/// Creates a [RapidOrderState].
|
||||
const RapidOrderState({
|
||||
this.status = RapidOrderStatus.initial,
|
||||
this.message = '',
|
||||
@@ -11,28 +13,42 @@ class RapidOrderState extends Equatable {
|
||||
this.isTranscribing = false,
|
||||
this.examples = const <String>[],
|
||||
this.error,
|
||||
this.parsedOrder,
|
||||
this.parsedDraft,
|
||||
});
|
||||
|
||||
/// Current status of the rapid order flow.
|
||||
final RapidOrderStatus status;
|
||||
|
||||
/// The text message entered or transcribed.
|
||||
final String message;
|
||||
|
||||
/// Whether the microphone is actively recording.
|
||||
final bool isListening;
|
||||
|
||||
/// Whether audio is being transcribed.
|
||||
final bool isTranscribing;
|
||||
|
||||
/// Example prompts for the user.
|
||||
final List<String> examples;
|
||||
|
||||
/// Error message, if any.
|
||||
final String? error;
|
||||
final OneTimeOrder? parsedOrder;
|
||||
|
||||
/// The parsed draft from the AI, as a map matching the V2 payload shape.
|
||||
final Map<String, dynamic>? parsedDraft;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
message,
|
||||
isListening,
|
||||
isTranscribing,
|
||||
examples,
|
||||
error,
|
||||
parsedOrder,
|
||||
];
|
||||
status,
|
||||
message,
|
||||
isListening,
|
||||
isTranscribing,
|
||||
examples,
|
||||
error,
|
||||
parsedDraft,
|
||||
];
|
||||
|
||||
/// Creates a copy with overridden fields.
|
||||
RapidOrderState copyWith({
|
||||
RapidOrderStatus? status,
|
||||
String? message,
|
||||
@@ -40,7 +56,7 @@ class RapidOrderState extends Equatable {
|
||||
bool? isTranscribing,
|
||||
List<String>? examples,
|
||||
String? error,
|
||||
OneTimeOrder? parsedOrder,
|
||||
Map<String, dynamic>? parsedDraft,
|
||||
}) {
|
||||
return RapidOrderState(
|
||||
status: status ?? this.status,
|
||||
@@ -49,7 +65,7 @@ class RapidOrderState extends Equatable {
|
||||
isTranscribing: isTranscribing ?? this.isTranscribing,
|
||||
examples: examples ?? this.examples,
|
||||
error: error ?? this.error,
|
||||
parsedOrder: parsedOrder ?? this.parsedOrder,
|
||||
parsedDraft: parsedDraft ?? this.parsedDraft,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:client_create_order/src/domain/usecases/create_recurring_order_u
|
||||
import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:client_create_order/src/domain/arguments/recurring_order_arguments.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import 'recurring_order_event.dart';
|
||||
@@ -13,10 +14,9 @@ import 'recurring_order_state.dart';
|
||||
|
||||
/// BLoC for managing the recurring order creation form.
|
||||
///
|
||||
/// This BLoC delegates all backend queries to
|
||||
/// [ClientOrderQueryRepositoryInterface] and order submission to
|
||||
/// [CreateRecurringOrderUseCase], keeping the presentation layer free
|
||||
/// from direct `krow_data_connect` imports.
|
||||
/// Delegates all backend queries to [ClientOrderQueryRepositoryInterface]
|
||||
/// and order submission to [CreateRecurringOrderUseCase].
|
||||
/// Builds V2 API payloads from form state.
|
||||
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
with
|
||||
BlocErrorHandler<RecurringOrderState>,
|
||||
@@ -111,9 +111,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
Future<void> _loadHubs() async {
|
||||
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String businessId = await _queryRepository.getBusinessId();
|
||||
final List<OrderHub> orderHubs =
|
||||
await _queryRepository.getHubsByOwner(businessId);
|
||||
final List<OrderHub> orderHubs = await _queryRepository.getHubs();
|
||||
return orderHubs
|
||||
.map(
|
||||
(OrderHub hub) => RecurringOrderHubOption(
|
||||
@@ -357,50 +355,56 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final Map<String, double> roleRates = <String, double>{
|
||||
for (final RecurringOrderRoleOption role in state.roles)
|
||||
role.id: role.costPerHour,
|
||||
};
|
||||
final RecurringOrderHubOption? selectedHub = state.selectedHub;
|
||||
if (selectedHub == null) {
|
||||
throw const domain.OrderMissingHubException();
|
||||
}
|
||||
final domain.RecurringOrder order = domain.RecurringOrder(
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
recurringDays: state.recurringDays,
|
||||
location: selectedHub.name,
|
||||
positions: state.positions
|
||||
.map(
|
||||
(RecurringOrderPosition p) => domain.RecurringOrderPosition(
|
||||
role: p.role,
|
||||
count: p.count,
|
||||
startTime: p.startTime,
|
||||
endTime: p.endTime,
|
||||
lunchBreak: p.lunchBreak ?? 'NO_BREAK',
|
||||
location: null,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
hub: domain.RecurringOrderHubDetails(
|
||||
id: selectedHub.id,
|
||||
name: selectedHub.name,
|
||||
address: selectedHub.address,
|
||||
placeId: selectedHub.placeId,
|
||||
latitude: selectedHub.latitude,
|
||||
longitude: selectedHub.longitude,
|
||||
city: selectedHub.city,
|
||||
state: selectedHub.state,
|
||||
street: selectedHub.street,
|
||||
country: selectedHub.country,
|
||||
zipCode: selectedHub.zipCode,
|
||||
),
|
||||
eventName: state.eventName,
|
||||
vendorId: state.selectedVendor?.id,
|
||||
hubManagerId: state.selectedManager?.id,
|
||||
roleRates: roleRates,
|
||||
|
||||
final String startDate =
|
||||
'${state.startDate.year.toString().padLeft(4, '0')}-'
|
||||
'${state.startDate.month.toString().padLeft(2, '0')}-'
|
||||
'${state.startDate.day.toString().padLeft(2, '0')}';
|
||||
final String endDate =
|
||||
'${state.endDate.year.toString().padLeft(4, '0')}-'
|
||||
'${state.endDate.month.toString().padLeft(2, '0')}-'
|
||||
'${state.endDate.day.toString().padLeft(2, '0')}';
|
||||
|
||||
// Map day labels (MON=1, TUE=2, ..., SUN=0) to V2 int format
|
||||
final List<int> recurrenceDays = state.recurringDays
|
||||
.map((String day) => _dayLabels.indexOf(day) % 7)
|
||||
.toList();
|
||||
|
||||
final List<Map<String, dynamic>> positions =
|
||||
state.positions.map((RecurringOrderPosition p) {
|
||||
final RecurringOrderRoleOption? role = state.roles
|
||||
.cast<RecurringOrderRoleOption?>()
|
||||
.firstWhere(
|
||||
(RecurringOrderRoleOption? r) => r != null && r.id == p.role,
|
||||
orElse: () => null,
|
||||
);
|
||||
return <String, dynamic>{
|
||||
if (role != null) 'roleName': role.name,
|
||||
if (p.role.isNotEmpty) 'roleId': p.role,
|
||||
'workerCount': p.count,
|
||||
'startTime': p.startTime,
|
||||
'endTime': p.endTime,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final Map<String, dynamic> payload = <String, dynamic>{
|
||||
'hubId': selectedHub.id,
|
||||
'eventName': state.eventName,
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
'recurrenceDays': recurrenceDays,
|
||||
'positions': positions,
|
||||
if (state.selectedVendor != null)
|
||||
'vendorId': state.selectedVendor!.id,
|
||||
};
|
||||
|
||||
await _createRecurringOrderUseCase(
|
||||
RecurringOrderArguments(payload: payload),
|
||||
);
|
||||
await _createRecurringOrderUseCase(order);
|
||||
emit(state.copyWith(status: RecurringOrderStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
@@ -430,53 +434,34 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final domain.ReorderData orderDetails =
|
||||
final domain.OrderPreview preview =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<RecurringOrderPosition> positions = orderDetails.positions
|
||||
.map((domain.ReorderPosition role) {
|
||||
return RecurringOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Update state with order details
|
||||
final domain.Vendor? selectedVendor = state.vendors
|
||||
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final RecurringOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(RecurringOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
// Map positions from preview shifts/roles
|
||||
final List<RecurringOrderPosition> positions =
|
||||
<RecurringOrderPosition>[];
|
||||
for (final domain.OrderPreviewShift shift in preview.shifts) {
|
||||
for (final domain.OrderPreviewRole role in shift.roles) {
|
||||
positions.add(RecurringOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.workersNeeded,
|
||||
startTime: _formatTime(shift.startsAt),
|
||||
endTime: _formatTime(shift.endsAt),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
eventName:
|
||||
preview.title.isNotEmpty ? preview.title : title,
|
||||
positions: positions.isNotEmpty ? positions : null,
|
||||
status: RecurringOrderStatus.initial,
|
||||
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
|
||||
endDate: orderDetails.endDate ?? DateTime.now(),
|
||||
recurringDays: orderDetails.recurringDays,
|
||||
startDate:
|
||||
startDate ?? preview.startsAt ?? DateTime.now(),
|
||||
endDate: preview.endsAt ?? DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: RecurringOrderStatus.failure,
|
||||
@@ -485,6 +470,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] to HH:mm string.
|
||||
String _formatTime(DateTime dt) {
|
||||
final DateTime local = dt.toLocal();
|
||||
return '${local.hour.toString().padLeft(2, '0')}:'
|
||||
'${local.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
static List<String> _sortDays(List<String> days) {
|
||||
days.sort(
|
||||
(String a, String b) =>
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide PermanentOrderPosition;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/permanent_order/permanent_order_bloc.dart';
|
||||
import '../blocs/permanent_order/permanent_order_event.dart';
|
||||
import '../blocs/permanent_order/permanent_order_state.dart';
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide RecurringOrderPosition;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/recurring_order/recurring_order_bloc.dart';
|
||||
import '../blocs/recurring_order/recurring_order_event.dart';
|
||||
import '../blocs/recurring_order/recurring_order_state.dart';
|
||||
|
||||
@@ -56,10 +56,10 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
||||
);
|
||||
}
|
||||
} else if (state.status == RapidOrderStatus.parsed &&
|
||||
state.parsedOrder != null) {
|
||||
state.parsedDraft != null) {
|
||||
Modular.to.toCreateOrderOneTime(
|
||||
arguments: <String, dynamic>{
|
||||
'order': state.parsedOrder,
|
||||
'order': state.parsedDraft,
|
||||
'isRapidDraft': true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -22,12 +22,8 @@ dependencies:
|
||||
path: ../../../../domain
|
||||
krow_core:
|
||||
path: ../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../../data_connect
|
||||
client_orders_common:
|
||||
path: ../orders_common
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -137,7 +137,7 @@ class OneTimeOrderForm extends StatelessWidget {
|
||||
return DropdownMenuItem<Vendor>(
|
||||
value: vendor,
|
||||
child: Text(
|
||||
vendor.name,
|
||||
vendor.companyName,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -147,7 +147,7 @@ class PermanentOrderForm extends StatelessWidget {
|
||||
return DropdownMenuItem<Vendor>(
|
||||
value: vendor,
|
||||
child: Text(
|
||||
vendor.name,
|
||||
vendor.companyName,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -155,7 +155,7 @@ class RecurringOrderForm extends StatelessWidget {
|
||||
return DropdownMenuItem<Vendor>(
|
||||
value: vendor,
|
||||
child: Text(
|
||||
vendor.name,
|
||||
vendor.companyName,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import Foundation
|
||||
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import firebase_app_check
|
||||
import firebase_auth
|
||||
import firebase_core
|
||||
import flutter_local_notifications
|
||||
@@ -19,7 +18,6 @@ import shared_preferences_foundation
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
|
||||
@@ -14,20 +14,18 @@ dependencies:
|
||||
flutter_bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
|
||||
intl: any
|
||||
|
||||
# Architecture Packages
|
||||
design_system:
|
||||
path: ../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../../core_localization
|
||||
krow_domain: ^0.0.1
|
||||
krow_data_connect: ^0.0.1
|
||||
krow_domain:
|
||||
path: ../../../../domain
|
||||
krow_core:
|
||||
path: ../../../../core
|
||||
|
||||
firebase_data_connect: any
|
||||
intl: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -1,199 +1,98 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/i_view_orders_repository.dart';
|
||||
|
||||
/// Implementation of [IViewOrdersRepository] using Data Connect.
|
||||
/// V2 API implementation of [IViewOrdersRepository].
|
||||
///
|
||||
/// Replaces the old Data Connect implementation with [BaseApiService] calls
|
||||
/// to the V2 query and command API endpoints.
|
||||
class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
|
||||
ViewOrdersRepositoryImpl({required dc.DataConnectService service})
|
||||
: _service = service;
|
||||
final dc.DataConnectService _service;
|
||||
/// Creates an instance backed by the given [apiService].
|
||||
ViewOrdersRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<domain.OrderItem>> getOrdersForRange({
|
||||
Future<List<OrderItem>> getOrdersForRange({
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
|
||||
final fdc.Timestamp startTimestamp = _service.toTimestamp(
|
||||
_startOfDay(start),
|
||||
);
|
||||
final fdc.Timestamp endTimestamp = _service.toTimestamp(_endOfDay(end));
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: startTimestamp,
|
||||
end: endTimestamp,
|
||||
)
|
||||
.execute();
|
||||
debugPrint(
|
||||
'ViewOrders range start=${start.toIso8601String()} end=${end.toIso8601String()} shiftRoles=${result.data.shiftRoles.length}',
|
||||
);
|
||||
|
||||
final String businessName =
|
||||
dc.ClientSessionStore.instance.session?.business?.businessName ??
|
||||
'Your Company';
|
||||
|
||||
return result.data.shiftRoles.map((
|
||||
dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole,
|
||||
) {
|
||||
final DateTime? shiftDate = shiftRole.shift.date
|
||||
?.toDateTime()
|
||||
.toLocal();
|
||||
final String dateStr = shiftDate == null
|
||||
? ''
|
||||
: DateFormat('yyyy-MM-dd').format(shiftDate);
|
||||
final String startTime = _formatTime(shiftRole.startTime);
|
||||
final String endTime = _formatTime(shiftRole.endTime);
|
||||
final int filled = shiftRole.assigned ?? 0;
|
||||
final int workersNeeded = shiftRole.count;
|
||||
final double hours = shiftRole.hours ?? 0;
|
||||
final double totalValue = shiftRole.totalValue ?? 0;
|
||||
final double hourlyRate = _hourlyRate(
|
||||
shiftRole.totalValue,
|
||||
shiftRole.hours,
|
||||
);
|
||||
// final String status = filled >= workersNeeded ? 'filled' : 'open';
|
||||
final String status = shiftRole.shift.status?.stringValue ?? 'OPEN';
|
||||
|
||||
debugPrint(
|
||||
'ViewOrders item: date=$dateStr status=$status shiftId=${shiftRole.shiftId} '
|
||||
'roleId=${shiftRole.roleId} start=${shiftRole.startTime?.toJson()} '
|
||||
'end=${shiftRole.endTime?.toJson()} hours=$hours totalValue=$totalValue',
|
||||
);
|
||||
|
||||
final String eventName =
|
||||
shiftRole.shift.order.eventName ?? shiftRole.shift.title;
|
||||
|
||||
final order = shiftRole.shift.order;
|
||||
final String? hubManagerId = order.hubManagerId;
|
||||
final String? hubManagerName = order.hubManager?.user?.fullName;
|
||||
|
||||
return domain.OrderItem(
|
||||
id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId),
|
||||
orderId: order.id,
|
||||
orderType: domain.OrderType.fromString(
|
||||
order.orderType.stringValue,
|
||||
),
|
||||
title: shiftRole.role.name,
|
||||
eventName: eventName,
|
||||
clientName: businessName,
|
||||
status: status,
|
||||
date: dateStr,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
location: shiftRole.shift.location ?? '',
|
||||
locationAddress: shiftRole.shift.locationAddress ?? '',
|
||||
filled: filled,
|
||||
workersNeeded: workersNeeded,
|
||||
hourlyRate: hourlyRate,
|
||||
hours: hours,
|
||||
totalValue: totalValue,
|
||||
confirmedApps: const <Map<String, dynamic>>[],
|
||||
hubManagerId: hubManagerId,
|
||||
hubManagerName: hubManagerName,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
final ApiResponse response = await _api.get(
|
||||
ClientEndpoints.ordersView,
|
||||
params: <String, dynamic>{
|
||||
'startDate': start.toIso8601String(),
|
||||
'endDate': end.toIso8601String(),
|
||||
},
|
||||
);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
OrderItem.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getAcceptedApplicationsForDay(
|
||||
DateTime day,
|
||||
) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
|
||||
final fdc.Timestamp dayStart = _service.toTimestamp(_startOfDay(day));
|
||||
final fdc.Timestamp dayEnd = _service.toTimestamp(_endOfDay(day));
|
||||
final fdc.QueryResult<
|
||||
dc.ListAcceptedApplicationsByBusinessForDayData,
|
||||
dc.ListAcceptedApplicationsByBusinessForDayVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listAcceptedApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: dayStart,
|
||||
dayEnd: dayEnd,
|
||||
)
|
||||
.execute();
|
||||
|
||||
print(
|
||||
'ViewOrders day=${day.toIso8601String()} applications=${result.data.applications.length}',
|
||||
);
|
||||
|
||||
final Map<String, List<Map<String, dynamic>>> grouped =
|
||||
<String, List<Map<String, dynamic>>>{};
|
||||
for (final dc.ListAcceptedApplicationsByBusinessForDayApplications
|
||||
application
|
||||
in result.data.applications) {
|
||||
print(
|
||||
'ViewOrders app: shiftId=${application.shiftId} roleId=${application.roleId} '
|
||||
'checkIn=${application.checkInTime?.toJson()} checkOut=${application.checkOutTime?.toJson()}',
|
||||
);
|
||||
final String key = _shiftRoleKey(
|
||||
application.shiftId,
|
||||
application.roleId,
|
||||
);
|
||||
grouped.putIfAbsent(key, () => <Map<String, dynamic>>[]);
|
||||
grouped[key]!.add(<String, dynamic>{
|
||||
'id': application.id,
|
||||
'worker_id': application.staff.id,
|
||||
'worker_name': application.staff.fullName,
|
||||
'status': 'confirmed',
|
||||
'photo_url': application.staff.photoUrl,
|
||||
'phone': application.staff.phone,
|
||||
'rating': application.staff.averageRating,
|
||||
});
|
||||
}
|
||||
return grouped;
|
||||
});
|
||||
Future<String> editOrder({
|
||||
required String orderId,
|
||||
required Map<String, dynamic> payload,
|
||||
}) async {
|
||||
final ApiResponse response = await _api.post(
|
||||
ClientEndpoints.orderEdit(orderId),
|
||||
data: payload,
|
||||
);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
return data['orderId'] as String? ?? orderId;
|
||||
}
|
||||
|
||||
String _shiftRoleKey(String shiftId, String roleId) {
|
||||
return '$shiftId:$roleId';
|
||||
}
|
||||
|
||||
DateTime _startOfDay(DateTime dateTime) {
|
||||
return DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
}
|
||||
|
||||
DateTime _endOfDay(DateTime dateTime) {
|
||||
// We add the current microseconds to ensure the query variables are unique
|
||||
// each time we fetch, bypassing any potential Data Connect caching.
|
||||
final DateTime now = DateTime.now();
|
||||
return DateTime(
|
||||
dateTime.year,
|
||||
dateTime.month,
|
||||
dateTime.day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
now.millisecond,
|
||||
now.microsecond,
|
||||
@override
|
||||
Future<void> cancelOrder({
|
||||
required String orderId,
|
||||
String? reason,
|
||||
}) async {
|
||||
await _api.post(
|
||||
ClientEndpoints.orderCancel(orderId),
|
||||
data: <String, dynamic>{
|
||||
if (reason != null) 'reason': reason,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(fdc.Timestamp? timestamp) {
|
||||
if (timestamp == null) {
|
||||
return '';
|
||||
}
|
||||
final DateTime dateTime = timestamp.toDateTime().toLocal();
|
||||
return DateFormat('HH:mm').format(dateTime);
|
||||
@override
|
||||
Future<List<Vendor>> getVendors() async {
|
||||
final ApiResponse response = await _api.get(ClientEndpoints.vendors);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) => Vendor.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
double _hourlyRate(double? totalValue, double? hours) {
|
||||
if (totalValue == null || hours == null || hours == 0) {
|
||||
return 0;
|
||||
}
|
||||
return totalValue / hours;
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getRolesByVendor(String vendorId) async {
|
||||
final ApiResponse response =
|
||||
await _api.get(ClientEndpoints.vendorRoles(vendorId));
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.cast<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getHubs() async {
|
||||
final ApiResponse response = await _api.get(ClientEndpoints.hubs);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.cast<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getManagersByHub(String hubId) async {
|
||||
final ApiResponse response =
|
||||
await _api.get(ClientEndpoints.hubManagers(hubId));
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = data['items'] as List<dynamic>;
|
||||
return items.cast<Map<String, dynamic>>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
class OrdersDayArguments extends UseCaseArgument {
|
||||
const OrdersDayArguments({required this.day});
|
||||
|
||||
final DateTime day;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[day];
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for fetching and managing client orders.
|
||||
///
|
||||
/// V2 API returns workers inline with order items, so the separate
|
||||
/// accepted-applications method is no longer needed.
|
||||
abstract class IViewOrdersRepository {
|
||||
/// Fetches a list of [OrderItem] for the client.
|
||||
/// Fetches [OrderItem] list for the given date range via the V2 API.
|
||||
Future<List<OrderItem>> getOrdersForRange({
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
});
|
||||
|
||||
/// Fetches accepted staff applications for the given day, grouped by shift+role.
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getAcceptedApplicationsForDay(
|
||||
DateTime day,
|
||||
);
|
||||
/// Submits an edit for the order identified by [orderId].
|
||||
///
|
||||
/// The [payload] map follows the V2 `clientOrderEdit` schema.
|
||||
/// The backend creates a new order copy and cancels the original.
|
||||
Future<String> editOrder({
|
||||
required String orderId,
|
||||
required Map<String, dynamic> payload,
|
||||
});
|
||||
|
||||
/// Cancels the order identified by [orderId].
|
||||
Future<void> cancelOrder({
|
||||
required String orderId,
|
||||
String? reason,
|
||||
});
|
||||
|
||||
/// Fetches available vendors for the current tenant.
|
||||
Future<List<Vendor>> getVendors();
|
||||
|
||||
/// Fetches roles offered by the given [vendorId].
|
||||
Future<List<Map<String, dynamic>>> getRolesByVendor(String vendorId);
|
||||
|
||||
/// Fetches hubs for the current business.
|
||||
Future<List<Map<String, dynamic>>> getHubs();
|
||||
|
||||
/// Fetches team members for the given [hubId].
|
||||
Future<List<Map<String, dynamic>>> getManagersByHub(String hubId);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/i_view_orders_repository.dart';
|
||||
import '../arguments/orders_day_arguments.dart';
|
||||
|
||||
class GetAcceptedApplicationsForDayUseCase
|
||||
implements UseCase<OrdersDayArguments, Map<String, List<Map<String, dynamic>>>> {
|
||||
const GetAcceptedApplicationsForDayUseCase(this._repository);
|
||||
|
||||
final IViewOrdersRepository _repository;
|
||||
|
||||
@override
|
||||
Future<Map<String, List<Map<String, dynamic>>>> call(
|
||||
OrdersDayArguments input,
|
||||
) {
|
||||
return _repository.getAcceptedApplicationsForDay(input.day);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,36 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/arguments/orders_day_arguments.dart';
|
||||
|
||||
import '../../domain/arguments/orders_range_arguments.dart';
|
||||
import '../../domain/usecases/get_accepted_applications_for_day_use_case.dart';
|
||||
import '../../domain/usecases/get_orders_use_case.dart';
|
||||
import 'view_orders_state.dart';
|
||||
|
||||
/// Cubit for managing the state of the View Orders feature.
|
||||
///
|
||||
/// This Cubit handles loading orders, date selection, and tab filtering.
|
||||
/// Handles loading orders, date selection, and tab filtering.
|
||||
/// V2 API returns workers inline so no separate applications fetch is needed.
|
||||
class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
with BlocErrorHandler<ViewOrdersState> {
|
||||
/// Creates the cubit with the required use case.
|
||||
ViewOrdersCubit({
|
||||
required GetOrdersUseCase getOrdersUseCase,
|
||||
required GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase,
|
||||
}) : _getOrdersUseCase = getOrdersUseCase,
|
||||
_getAcceptedAppsUseCase = getAcceptedAppsUseCase,
|
||||
super(ViewOrdersState(selectedDate: DateTime.now())) {
|
||||
}) : _getOrdersUseCase = getOrdersUseCase,
|
||||
super(ViewOrdersState(selectedDate: DateTime.now())) {
|
||||
_init();
|
||||
}
|
||||
|
||||
final GetOrdersUseCase _getOrdersUseCase;
|
||||
final GetAcceptedApplicationsForDayUseCase _getAcceptedAppsUseCase;
|
||||
int _requestId = 0;
|
||||
|
||||
void _init() {
|
||||
updateWeekOffset(0); // Initialize calendar days
|
||||
updateWeekOffset(0);
|
||||
}
|
||||
|
||||
/// Loads orders for the given date range.
|
||||
Future<void> _loadOrdersForRange({
|
||||
required DateTime rangeStart,
|
||||
required DateTime rangeEnd,
|
||||
required DateTime dayForApps,
|
||||
}) async {
|
||||
final int requestId = ++_requestId;
|
||||
emit(state.copyWith(status: ViewOrdersStatus.loading));
|
||||
@@ -46,18 +43,13 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
final List<OrderItem> orders = await _getOrdersUseCase(
|
||||
OrdersRangeArguments(start: rangeStart, end: rangeEnd),
|
||||
);
|
||||
final Map<String, List<Map<String, dynamic>>> apps =
|
||||
await _getAcceptedAppsUseCase(OrdersDayArguments(day: dayForApps));
|
||||
|
||||
if (requestId != _requestId) {
|
||||
return;
|
||||
}
|
||||
if (requestId != _requestId) return;
|
||||
|
||||
final List<OrderItem> updatedOrders = _applyApplications(orders, apps);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ViewOrdersStatus.success,
|
||||
orders: updatedOrders,
|
||||
orders: orders,
|
||||
),
|
||||
);
|
||||
_updateDerivedState();
|
||||
@@ -69,25 +61,28 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Selects a date and refilters.
|
||||
void selectDate(DateTime date) {
|
||||
emit(state.copyWith(selectedDate: date));
|
||||
_refreshAcceptedApplications(date);
|
||||
_updateDerivedState();
|
||||
}
|
||||
|
||||
/// Selects a filter tab and refilters.
|
||||
void selectFilterTab(String tabId) {
|
||||
emit(state.copyWith(filterTab: tabId));
|
||||
_updateDerivedState();
|
||||
}
|
||||
|
||||
/// Navigates the calendar by week offset.
|
||||
void updateWeekOffset(int offset) {
|
||||
final int newWeekOffset = state.weekOffset + offset;
|
||||
final List<DateTime> calendarDays = _calculateCalendarDays(newWeekOffset);
|
||||
final DateTime? selectedDate = state.selectedDate;
|
||||
final DateTime updatedSelectedDate =
|
||||
selectedDate != null &&
|
||||
calendarDays.any((DateTime day) => _isSameDay(day, selectedDate))
|
||||
? selectedDate
|
||||
: calendarDays.first;
|
||||
calendarDays.any((DateTime day) => _isSameDay(day, selectedDate))
|
||||
? selectedDate
|
||||
: calendarDays.first;
|
||||
emit(
|
||||
state.copyWith(
|
||||
weekOffset: newWeekOffset,
|
||||
@@ -99,10 +94,10 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
_loadOrdersForRange(
|
||||
rangeStart: calendarDays.first,
|
||||
rangeEnd: calendarDays.last,
|
||||
dayForApps: updatedSelectedDate,
|
||||
);
|
||||
}
|
||||
|
||||
/// Jumps the calendar to a specific date.
|
||||
void jumpToDate(DateTime date) {
|
||||
final DateTime target = DateTime(date.year, date.month, date.day);
|
||||
final DateTime startDate = _calculateCalendarDays(0).first;
|
||||
@@ -121,14 +116,13 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
_loadOrdersForRange(
|
||||
rangeStart: calendarDays.first,
|
||||
rangeEnd: calendarDays.last,
|
||||
dayForApps: target,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateDerivedState() {
|
||||
final List<OrderItem> filteredOrders = _calculateFilteredOrders(state);
|
||||
final int activeCount = _calculateCategoryCount('active');
|
||||
final int completedCount = _calculateCategoryCount('completed');
|
||||
final int activeCount = _calculateCategoryCount(ShiftStatus.active);
|
||||
final int completedCount = _calculateCategoryCount(ShiftStatus.completed);
|
||||
final int upNextCount = _calculateUpNextCount();
|
||||
|
||||
emit(
|
||||
@@ -141,64 +135,6 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshAcceptedApplications(DateTime day) async {
|
||||
await handleErrorWithResult(
|
||||
action: () async {
|
||||
final Map<String, List<Map<String, dynamic>>> apps =
|
||||
await _getAcceptedAppsUseCase(OrdersDayArguments(day: day));
|
||||
final List<OrderItem> updatedOrders = _applyApplications(
|
||||
state.orders,
|
||||
apps,
|
||||
);
|
||||
emit(state.copyWith(orders: updatedOrders));
|
||||
_updateDerivedState();
|
||||
},
|
||||
onError: (_) {
|
||||
// Keep existing data on failure, just log error via handleErrorWithResult
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<OrderItem> _applyApplications(
|
||||
List<OrderItem> orders,
|
||||
Map<String, List<Map<String, dynamic>>> apps,
|
||||
) {
|
||||
return orders.map((OrderItem order) {
|
||||
final List<Map<String, dynamic>> confirmed =
|
||||
apps[order.id] ?? const <Map<String, dynamic>>[];
|
||||
if (confirmed.isEmpty) {
|
||||
return order;
|
||||
}
|
||||
|
||||
final int filled = confirmed.length;
|
||||
final String status = filled >= order.workersNeeded
|
||||
? 'FILLED'
|
||||
: order.status;
|
||||
return OrderItem(
|
||||
id: order.id,
|
||||
orderId: order.orderId,
|
||||
orderType: order.orderType,
|
||||
title: order.title,
|
||||
eventName: order.eventName,
|
||||
clientName: order.clientName,
|
||||
status: status,
|
||||
date: order.date,
|
||||
startTime: order.startTime,
|
||||
endTime: order.endTime,
|
||||
location: order.location,
|
||||
locationAddress: order.locationAddress,
|
||||
filled: filled,
|
||||
workersNeeded: order.workersNeeded,
|
||||
hourlyRate: order.hourlyRate,
|
||||
hours: order.hours,
|
||||
totalValue: order.totalValue,
|
||||
confirmedApps: confirmed,
|
||||
hubManagerId: order.hubManagerId,
|
||||
hubManagerName: order.hubManagerName,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool _isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
@@ -218,103 +154,64 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Filters orders for the selected date and tab.
|
||||
List<OrderItem> _calculateFilteredOrders(ViewOrdersState state) {
|
||||
if (state.selectedDate == null) return <OrderItem>[];
|
||||
|
||||
final String selectedDateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(state.selectedDate!);
|
||||
final DateTime selectedDay = state.selectedDate!;
|
||||
|
||||
// Filter by date
|
||||
final List<OrderItem> ordersOnDate = state.orders
|
||||
.where((OrderItem s) => s.date == selectedDateStr)
|
||||
.where((OrderItem s) => _isSameDay(s.date, selectedDay))
|
||||
.toList();
|
||||
|
||||
// Sort by start time
|
||||
ordersOnDate.sort(
|
||||
(OrderItem a, OrderItem b) => a.startTime.compareTo(b.startTime),
|
||||
(OrderItem a, OrderItem b) => a.startsAt.compareTo(b.startsAt),
|
||||
);
|
||||
|
||||
if (state.filterTab == 'all') {
|
||||
final List<OrderItem> filtered = ordersOnDate
|
||||
return ordersOnDate
|
||||
.where(
|
||||
(OrderItem s) =>
|
||||
// TODO(orders): move PENDING to its own tab once available.
|
||||
<String>[
|
||||
'OPEN',
|
||||
'FILLED',
|
||||
'CONFIRMED',
|
||||
'PENDING',
|
||||
'ASSIGNED',
|
||||
].contains(s.status),
|
||||
(OrderItem s) => <ShiftStatus>[
|
||||
ShiftStatus.open,
|
||||
ShiftStatus.pendingConfirmation,
|
||||
ShiftStatus.assigned,
|
||||
].contains(s.status),
|
||||
)
|
||||
.toList();
|
||||
print(
|
||||
'ViewOrders tab=all statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
|
||||
);
|
||||
return filtered;
|
||||
} else if (state.filterTab == 'active') {
|
||||
final List<OrderItem> filtered = ordersOnDate
|
||||
.where((OrderItem s) => s.status == 'IN_PROGRESS')
|
||||
return ordersOnDate
|
||||
.where((OrderItem s) => s.status == ShiftStatus.active)
|
||||
.toList();
|
||||
print(
|
||||
'ViewOrders tab=active statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
|
||||
);
|
||||
return filtered;
|
||||
} else if (state.filterTab == 'completed') {
|
||||
final List<OrderItem> filtered = ordersOnDate
|
||||
.where((OrderItem s) => s.status == 'COMPLETED')
|
||||
return ordersOnDate
|
||||
.where((OrderItem s) => s.status == ShiftStatus.completed)
|
||||
.toList();
|
||||
print(
|
||||
'ViewOrders tab=completed statuses=${ordersOnDate.map((OrderItem s) => s.status).toList()} filtered=${filtered.length}',
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
return <OrderItem>[];
|
||||
}
|
||||
|
||||
int _calculateCategoryCount(String category) {
|
||||
int _calculateCategoryCount(ShiftStatus targetStatus) {
|
||||
if (state.selectedDate == null) return 0;
|
||||
|
||||
final String selectedDateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(state.selectedDate!);
|
||||
|
||||
if (category == 'active') {
|
||||
return state.orders
|
||||
.where(
|
||||
(OrderItem s) =>
|
||||
s.date == selectedDateStr && s.status == 'IN_PROGRESS',
|
||||
)
|
||||
.length;
|
||||
} else if (category == 'completed') {
|
||||
return state.orders
|
||||
.where(
|
||||
(OrderItem s) =>
|
||||
s.date == selectedDateStr && s.status == 'COMPLETED',
|
||||
)
|
||||
.length;
|
||||
}
|
||||
return 0;
|
||||
final DateTime selectedDay = state.selectedDate!;
|
||||
return state.orders
|
||||
.where(
|
||||
(OrderItem s) =>
|
||||
_isSameDay(s.date, selectedDay) && s.status == targetStatus,
|
||||
)
|
||||
.length;
|
||||
}
|
||||
|
||||
int _calculateUpNextCount() {
|
||||
if (state.selectedDate == null) return 0;
|
||||
|
||||
final String selectedDateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(state.selectedDate!);
|
||||
|
||||
final DateTime selectedDay = state.selectedDate!;
|
||||
return state.orders
|
||||
.where(
|
||||
(OrderItem s) =>
|
||||
s.date == selectedDateStr &&
|
||||
<String>[
|
||||
'OPEN',
|
||||
'FILLED',
|
||||
'CONFIRMED',
|
||||
'PENDING',
|
||||
'ASSIGNED',
|
||||
_isSameDay(s.date, selectedDay) &&
|
||||
<ShiftStatus>[
|
||||
ShiftStatus.open,
|
||||
ShiftStatus.pendingConfirmation,
|
||||
ShiftStatus.assigned,
|
||||
].contains(s.status),
|
||||
)
|
||||
.length;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,16 +3,12 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../blocs/view_orders_cubit.dart';
|
||||
|
||||
/// A rich card displaying details of a client order/shift.
|
||||
///
|
||||
/// This widget complies with the KROW Design System by using
|
||||
/// tokens from `package:design_system`.
|
||||
import 'order_edit_sheet.dart';
|
||||
|
||||
/// A rich card displaying details of a V2 [OrderItem].
|
||||
///
|
||||
/// Uses DateTime-based fields and [AssignedWorkerSummary] workers list.
|
||||
class ViewOrderCard extends StatefulWidget {
|
||||
/// Creates a [ViewOrderCard] for the given [order].
|
||||
const ViewOrderCard({required this.order, super.key});
|
||||
@@ -41,18 +37,18 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
}
|
||||
|
||||
/// Returns the semantic color for the given status.
|
||||
Color _getStatusColor({required String status}) {
|
||||
Color _getStatusColor({required ShiftStatus status}) {
|
||||
switch (status) {
|
||||
case 'OPEN':
|
||||
case ShiftStatus.open:
|
||||
return UiColors.primary;
|
||||
case 'FILLED':
|
||||
case 'CONFIRMED':
|
||||
case ShiftStatus.assigned:
|
||||
case ShiftStatus.pendingConfirmation:
|
||||
return UiColors.textSuccess;
|
||||
case 'IN_PROGRESS':
|
||||
case ShiftStatus.active:
|
||||
return UiColors.textWarning;
|
||||
case 'COMPLETED':
|
||||
case ShiftStatus.completed:
|
||||
return UiColors.primary;
|
||||
case 'CANCELED':
|
||||
case ShiftStatus.cancelled:
|
||||
return UiColors.destructive;
|
||||
default:
|
||||
return UiColors.textSecondary;
|
||||
@@ -60,41 +56,42 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
}
|
||||
|
||||
/// Returns the localized label for the given status.
|
||||
String _getStatusLabel({required String status}) {
|
||||
String _getStatusLabel({required ShiftStatus status}) {
|
||||
switch (status) {
|
||||
case 'OPEN':
|
||||
case ShiftStatus.open:
|
||||
return t.client_view_orders.card.open;
|
||||
case 'FILLED':
|
||||
case ShiftStatus.assigned:
|
||||
return t.client_view_orders.card.filled;
|
||||
case 'CONFIRMED':
|
||||
case ShiftStatus.pendingConfirmation:
|
||||
return t.client_view_orders.card.confirmed;
|
||||
case 'IN_PROGRESS':
|
||||
case ShiftStatus.active:
|
||||
return t.client_view_orders.card.in_progress;
|
||||
case 'COMPLETED':
|
||||
case ShiftStatus.completed:
|
||||
return t.client_view_orders.card.completed;
|
||||
case 'CANCELED':
|
||||
case ShiftStatus.cancelled:
|
||||
return t.client_view_orders.card.cancelled;
|
||||
default:
|
||||
return status.toUpperCase();
|
||||
return status.value.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the time string for display.
|
||||
String _formatTime({required String timeStr}) {
|
||||
if (timeStr.isEmpty) return '';
|
||||
try {
|
||||
final List<String> parts = timeStr.split(':');
|
||||
int hour = int.parse(parts[0]);
|
||||
final int minute = int.parse(parts[1]);
|
||||
final String ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
hour = hour % 12;
|
||||
if (hour == 0) hour = 12;
|
||||
return '$hour:${minute.toString().padLeft(2, '0')} $ampm';
|
||||
} catch (_) {
|
||||
return timeStr;
|
||||
}
|
||||
/// Formats a [DateTime] to a display time string (e.g. "9:00 AM").
|
||||
String _formatTime({required DateTime dateTime}) {
|
||||
final DateTime local = dateTime.toLocal();
|
||||
final int hour24 = local.hour;
|
||||
final int minute = local.minute;
|
||||
final String ampm = hour24 >= 12 ? 'PM' : 'AM';
|
||||
int hour = hour24 % 12;
|
||||
if (hour == 0) hour = 12;
|
||||
return '$hour:${minute.toString().padLeft(2, '0')} $ampm';
|
||||
}
|
||||
|
||||
/// Computes the duration in hours between start and end.
|
||||
double _computeHours(OrderItem order) {
|
||||
return order.endsAt.difference(order.startsAt).inMinutes / 60.0;
|
||||
}
|
||||
|
||||
/// Returns the order type display label.
|
||||
String _getOrderTypeLabel(OrderType type) {
|
||||
switch (type) {
|
||||
case OrderType.oneTime:
|
||||
@@ -105,44 +102,16 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
return 'RECURRING';
|
||||
case OrderType.rapid:
|
||||
return 'RAPID';
|
||||
case OrderType.unknown:
|
||||
return 'ORDER';
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the edit icon should be shown.
|
||||
/// Hidden for completed orders and for past orders (shift has ended).
|
||||
bool _canEditOrder(OrderItem order) {
|
||||
if (order.status == 'COMPLETED') return false;
|
||||
if (order.date.isEmpty) return true;
|
||||
try {
|
||||
final DateTime orderDate = DateTime.parse(order.date);
|
||||
final String endTime = order.endTime.trim();
|
||||
final DateTime endDateTime;
|
||||
if (endTime.isEmpty) {
|
||||
// No end time: use end of day so orders today remain editable
|
||||
endDateTime = DateTime(
|
||||
orderDate.year,
|
||||
orderDate.month,
|
||||
orderDate.day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
} else {
|
||||
final List<String> endParts = endTime.split(':');
|
||||
final int hour = endParts.isNotEmpty ? int.parse(endParts[0]) : 0;
|
||||
final int minute = endParts.length > 1 ? int.parse(endParts[1]) : 0;
|
||||
endDateTime = DateTime(
|
||||
orderDate.year,
|
||||
orderDate.month,
|
||||
orderDate.day,
|
||||
hour,
|
||||
minute,
|
||||
);
|
||||
}
|
||||
return endDateTime.isAfter(DateTime.now());
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
if (order.status == ShiftStatus.completed) return false;
|
||||
if (order.status == ShiftStatus.cancelled) return false;
|
||||
return order.endsAt.isAfter(DateTime.now());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -150,12 +119,12 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
final OrderItem order = widget.order;
|
||||
final Color statusColor = _getStatusColor(status: order.status);
|
||||
final String statusLabel = _getStatusLabel(status: order.status);
|
||||
final int coveragePercent = order.workersNeeded > 0
|
||||
? ((order.filled / order.workersNeeded) * 100).round()
|
||||
final int coveragePercent = order.requiredWorkerCount > 0
|
||||
? ((order.filledCount / order.requiredWorkerCount) * 100).round()
|
||||
: 0;
|
||||
|
||||
final double hours = order.hours;
|
||||
final double cost = order.totalValue;
|
||||
final double hours = _computeHours(order);
|
||||
final double cost = order.totalCostCents / 100.0;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -232,92 +201,28 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
// Title
|
||||
Text(order.title, style: UiTypography.headline3b),
|
||||
Row(
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.calendarCheck,
|
||||
size: 14,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.eventName,
|
||||
style: UiTypography.headline5m.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
// Location (Hub name + Address)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 2),
|
||||
child: Icon(
|
||||
// Title (role name)
|
||||
Text(order.roleName, style: UiTypography.headline3b),
|
||||
if (order.locationName != null &&
|
||||
order.locationName!.isNotEmpty)
|
||||
Row(
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
size: 14,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (order.location.isNotEmpty)
|
||||
Text(
|
||||
order.location,
|
||||
style: UiTypography
|
||||
.footnote1b
|
||||
.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (order.locationAddress.isNotEmpty)
|
||||
Text(
|
||||
order.locationAddress,
|
||||
style: UiTypography
|
||||
.footnote2r
|
||||
.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (order.hubManagerName != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 2),
|
||||
child: Icon(
|
||||
UiIcons.user,
|
||||
size: 14,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.hubManagerName!,
|
||||
order.locationName!,
|
||||
style:
|
||||
UiTypography.footnote2r.textSecondary,
|
||||
maxLines: 1,
|
||||
UiTypography.headline5m.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -334,7 +239,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
),
|
||||
if (_canEditOrder(order))
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
if (order.confirmedApps.isNotEmpty)
|
||||
if (order.workers.isNotEmpty)
|
||||
_buildHeaderIconButton(
|
||||
icon: _expanded
|
||||
? UiIcons.chevronUp
|
||||
@@ -374,7 +279,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
_buildStatDivider(),
|
||||
_buildStatItem(
|
||||
icon: UiIcons.users,
|
||||
value: '${order.workersNeeded}',
|
||||
value: '${order.requiredWorkerCount}',
|
||||
label: t.client_create_order.one_time.workers_label,
|
||||
),
|
||||
],
|
||||
@@ -389,14 +294,14 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
Expanded(
|
||||
child: _buildTimeDisplay(
|
||||
label: t.client_view_orders.card.clock_in,
|
||||
time: _formatTime(timeStr: order.startTime),
|
||||
time: _formatTime(dateTime: order.startsAt),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: _buildTimeDisplay(
|
||||
label: t.client_view_orders.card.clock_out,
|
||||
time: _formatTime(timeStr: order.endTime),
|
||||
time: _formatTime(dateTime: order.endsAt),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -405,7 +310,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Coverage Section
|
||||
if (order.status != 'completed') ...<Widget>[
|
||||
if (order.status != ShiftStatus.completed) ...<Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
@@ -428,7 +333,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
coveragePercent == 100
|
||||
? t.client_view_orders.card.all_confirmed
|
||||
: t.client_view_orders.card.workers_needed(
|
||||
count: order.workersNeeded,
|
||||
count: order.requiredWorkerCount,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
@@ -456,17 +361,17 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
),
|
||||
|
||||
// Avatar Stack Preview (if not expanded)
|
||||
if (!_expanded && order.confirmedApps.isNotEmpty) ...<Widget>[
|
||||
if (!_expanded && order.workers.isNotEmpty) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
_buildAvatarStack(order.confirmedApps),
|
||||
if (order.confirmedApps.length > 3)
|
||||
_buildAvatarStack(order.workers),
|
||||
if (order.workers.length > 3)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Text(
|
||||
t.client_view_orders.card.show_more_workers(
|
||||
count: order.confirmedApps.length - 3,
|
||||
count: order.workers.length - 3,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
@@ -480,7 +385,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
),
|
||||
|
||||
// Assigned Workers (Expanded section)
|
||||
if (_expanded && order.confirmedApps.isNotEmpty) ...<Widget>[
|
||||
if (_expanded && order.workers.isNotEmpty) ...<Widget>[
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
@@ -512,10 +417,12 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
...order.confirmedApps
|
||||
...order.workers
|
||||
.take(5)
|
||||
.map((Map<String, dynamic> app) => _buildWorkerRow(app)),
|
||||
if (order.confirmedApps.length > 5)
|
||||
.map(
|
||||
(AssignedWorkerSummary w) => _buildWorkerRow(w),
|
||||
),
|
||||
if (order.workers.length > 5)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Center(
|
||||
@@ -523,7 +430,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
t.client_view_orders.card.show_more_workers(
|
||||
count: order.confirmedApps.length - 5,
|
||||
count: order.workers.length - 5,
|
||||
),
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
@@ -541,10 +448,12 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a stat divider.
|
||||
Widget _buildStatDivider() {
|
||||
return Container(width: 1, height: 24, color: UiColors.border);
|
||||
}
|
||||
|
||||
/// Builds a time display box.
|
||||
Widget _buildTimeDisplay({required String label, required String time}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
@@ -565,11 +474,11 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a stacked avatar UI for a list of applications.
|
||||
Widget _buildAvatarStack(List<Map<String, dynamic>> apps) {
|
||||
/// Builds a stacked avatar UI for assigned workers.
|
||||
Widget _buildAvatarStack(List<AssignedWorkerSummary> workers) {
|
||||
const double size = 32.0;
|
||||
const double overlap = 22.0;
|
||||
final int count = apps.length > 3 ? 3 : apps.length;
|
||||
final int count = workers.length > 3 ? 3 : workers.length;
|
||||
|
||||
return SizedBox(
|
||||
height: size,
|
||||
@@ -589,7 +498,9 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
(apps[i]['worker_name'] as String)[0],
|
||||
(workers[i].workerName ?? '').isNotEmpty
|
||||
? (workers[i].workerName ?? '?')[0]
|
||||
: '?',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
@@ -603,8 +514,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
}
|
||||
|
||||
/// Builds a detailed row for a worker.
|
||||
Widget _buildWorkerRow(Map<String, dynamic> app) {
|
||||
final String? phone = app['phone'] as String?;
|
||||
Widget _buildWorkerRow(AssignedWorkerSummary worker) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
@@ -618,7 +528,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
CircleAvatar(
|
||||
backgroundColor: UiColors.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
(app['worker_name'] as String)[0],
|
||||
(worker.workerName ?? '').isNotEmpty ? (worker.workerName ?? '?')[0] : '?',
|
||||
style: UiTypography.body1b.copyWith(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
@@ -628,129 +538,35 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
app['worker_name'] as String,
|
||||
worker.workerName ?? '',
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1 / 2),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if ((app['rating'] as num?) != null &&
|
||||
(app['rating'] as num) > 0) ...<Widget>[
|
||||
const Icon(
|
||||
UiIcons.star,
|
||||
size: 10,
|
||||
color: UiColors.accent,
|
||||
if (worker.confirmationStatus != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
),
|
||||
child: Text(
|
||||
worker.confirmationStatus!.value.toUpperCase(),
|
||||
style: UiTypography.titleUppercase4m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
(app['rating'] as num).toStringAsFixed(1),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
if (app['check_in_time'] != null) ...<Widget>[
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.textSuccess.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
),
|
||||
child: Text(
|
||||
t.client_view_orders.card.checked_in,
|
||||
style: UiTypography.titleUppercase4m.copyWith(
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if ((app['status'] as String?)?.isNotEmpty ??
|
||||
false) ...<Widget>[
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
),
|
||||
child: Text(
|
||||
(app['status'] as String).toUpperCase(),
|
||||
style: UiTypography.titleUppercase4m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (phone != null && phone.isNotEmpty) ...<Widget>[
|
||||
_buildActionIconButton(
|
||||
icon: UiIcons.phone,
|
||||
onTap: () => _confirmAndCall(phone),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmAndCall(String phone) async {
|
||||
final bool? shouldCall = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(t.client_view_orders.card.call_dialog.title),
|
||||
content: Text(
|
||||
t.client_view_orders.card.call_dialog.message(phone: phone),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(t.client_view_orders.card.call_dialog.title),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (shouldCall != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Uri uri = Uri(scheme: 'tel', path: phone);
|
||||
await launchUrl(uri);
|
||||
}
|
||||
|
||||
/// Specialized action button for worker rows.
|
||||
Widget _buildActionIconButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: BorderRadius.circular(UiConstants.space2),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: UiColors.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a small icon button used in row headers.
|
||||
Widget _buildHeaderIconButton({
|
||||
required IconData icon,
|
||||
@@ -771,7 +587,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a single stat item (e.g., Cost, Hours, Workers).
|
||||
/// Builds a single stat item.
|
||||
Widget _buildStatItem({
|
||||
required IconData icon,
|
||||
required String value,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
@@ -49,6 +48,13 @@ class ViewOrdersEmptyState extends StatelessWidget {
|
||||
|
||||
if (checkDate == today) return 'Today';
|
||||
if (checkDate == tomorrow) return 'Tomorrow';
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
const List<String> weekdays = <String>[
|
||||
'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun',
|
||||
];
|
||||
const List<String> months = <String>[
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
||||
];
|
||||
return '${weekdays[date.weekday - 1]}, ${months[date.month - 1]} ${date.day}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -32,6 +31,25 @@ class ViewOrdersHeader extends StatelessWidget {
|
||||
/// The list of calendar days to display.
|
||||
final List<DateTime> calendarDays;
|
||||
|
||||
static const List<String> _months = <String>[
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
|
||||
static const List<String> _weekdays = <String>[
|
||||
'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun',
|
||||
];
|
||||
|
||||
/// Formats a date as "Month YYYY".
|
||||
static String _formatMonthYear(DateTime date) {
|
||||
return '${_months[date.month - 1]} ${date.year}';
|
||||
}
|
||||
|
||||
/// Returns the abbreviated weekday name.
|
||||
static String _weekdayAbbr(int weekday) {
|
||||
return _weekdays[weekday - 1];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRect(
|
||||
@@ -133,7 +151,7 @@ class ViewOrdersHeader extends StatelessWidget {
|
||||
splashRadius: UiConstants.iconMd,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(calendarDays.first),
|
||||
_formatMonthYear(calendarDays.first),
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
@@ -175,11 +193,11 @@ class ViewOrdersHeader extends StatelessWidget {
|
||||
date.day == state.selectedDate!.day;
|
||||
|
||||
// Check if this date has any shifts
|
||||
final String dateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(date);
|
||||
final bool hasShifts = state.orders.any(
|
||||
(OrderItem s) => s.date == dateStr,
|
||||
(OrderItem s) =>
|
||||
s.date.year == date.year &&
|
||||
s.date.month == date.month &&
|
||||
s.date.day == date.day,
|
||||
);
|
||||
|
||||
// Check if date is in the past
|
||||
@@ -221,7 +239,7 @@ class ViewOrdersHeader extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd').format(date),
|
||||
date.day.toString().padLeft(2, '0'),
|
||||
style: UiTypography.title1m.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
@@ -230,7 +248,7 @@ class ViewOrdersHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
_weekdayAbbr(date.weekday),
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: isSelected
|
||||
? UiColors.white.withValues(alpha: 0.8)
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'data/repositories/view_orders_repository_impl.dart';
|
||||
import 'domain/repositories/i_view_orders_repository.dart';
|
||||
import 'domain/usecases/get_accepted_applications_for_day_use_case.dart';
|
||||
import 'domain/usecases/get_orders_use_case.dart';
|
||||
import 'presentation/blocs/view_orders_cubit.dart';
|
||||
import 'presentation/pages/view_orders_page.dart';
|
||||
|
||||
/// Module for the View Orders feature.
|
||||
///
|
||||
/// This module sets up Dependency Injection for repositories, use cases,
|
||||
/// and BLoCs, and defines the feature's navigation routes.
|
||||
/// Sets up DI for repositories, use cases, and BLoCs, and defines routes.
|
||||
/// Uses [CoreModule] for [BaseApiService] injection (V2 API).
|
||||
class ViewOrdersModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.add<IViewOrdersRepository>(ViewOrdersRepositoryImpl.new);
|
||||
i.add<IViewOrdersRepository>(
|
||||
() => ViewOrdersRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.add(GetOrdersUseCase.new);
|
||||
i.add(GetAcceptedApplicationsForDayUseCase.new);
|
||||
|
||||
// BLoCs
|
||||
i.addLazySingleton(ViewOrdersCubit.new);
|
||||
@@ -38,11 +40,11 @@ class ViewOrdersModule extends Module {
|
||||
child: (BuildContext context) {
|
||||
final Object? args = Modular.args.data;
|
||||
DateTime? initialDate;
|
||||
|
||||
// Try parsing from args.data first
|
||||
|
||||
if (args is DateTime) {
|
||||
initialDate = args;
|
||||
} else if (args is Map<String, dynamic> && args['initialDate'] != null) {
|
||||
} else if (args is Map<String, dynamic> &&
|
||||
args['initialDate'] != null) {
|
||||
final Object? rawDate = args['initialDate'];
|
||||
if (rawDate is DateTime) {
|
||||
initialDate = rawDate;
|
||||
@@ -50,15 +52,14 @@ class ViewOrdersModule extends Module {
|
||||
initialDate = DateTime.tryParse(rawDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to query params
|
||||
|
||||
if (initialDate == null) {
|
||||
final String? queryDate = Modular.args.queryParams['initialDate'];
|
||||
if (queryDate != null && queryDate.isNotEmpty) {
|
||||
initialDate = DateTime.tryParse(queryDate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ViewOrdersPage(initialDate: initialDate);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -25,13 +25,9 @@ dependencies:
|
||||
path: ../../../../domain
|
||||
krow_core:
|
||||
path: ../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../../data_connect
|
||||
|
||||
# UI
|
||||
intl: ^0.20.1
|
||||
url_launcher: ^6.3.1
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user