first version of recurring order
This commit is contained in:
@@ -37,6 +37,8 @@ export 'src/adapters/shifts/break/break_adapter.dart';
|
|||||||
export 'src/entities/orders/order_type.dart';
|
export 'src/entities/orders/order_type.dart';
|
||||||
export 'src/entities/orders/one_time_order.dart';
|
export 'src/entities/orders/one_time_order.dart';
|
||||||
export 'src/entities/orders/one_time_order_position.dart';
|
export 'src/entities/orders/one_time_order_position.dart';
|
||||||
|
export 'src/entities/orders/recurring_order.dart';
|
||||||
|
export 'src/entities/orders/recurring_order_position.dart';
|
||||||
export 'src/entities/orders/order_item.dart';
|
export 'src/entities/orders/order_item.dart';
|
||||||
|
|
||||||
// Skills & Certs
|
// Skills & Certs
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'recurring_order_position.dart';
|
||||||
|
|
||||||
|
/// Represents a recurring staffing request spanning a date range.
|
||||||
|
class RecurringOrder extends Equatable {
|
||||||
|
const RecurringOrder({
|
||||||
|
required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
required this.recurringDays,
|
||||||
|
required this.location,
|
||||||
|
required this.positions,
|
||||||
|
this.hub,
|
||||||
|
this.eventName,
|
||||||
|
this.vendorId,
|
||||||
|
this.roleRates = const <String, double>{},
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Start date for the recurring schedule.
|
||||||
|
final DateTime startDate;
|
||||||
|
|
||||||
|
/// End date for the recurring schedule.
|
||||||
|
final DateTime endDate;
|
||||||
|
|
||||||
|
/// Days of the week to repeat on (e.g., ["S", "M", ...]).
|
||||||
|
final List<String> recurringDays;
|
||||||
|
|
||||||
|
/// The primary location where the work will take place.
|
||||||
|
final String location;
|
||||||
|
|
||||||
|
/// The list of positions and headcounts required for this order.
|
||||||
|
final List<RecurringOrderPosition> positions;
|
||||||
|
|
||||||
|
/// Selected hub details for this order.
|
||||||
|
final RecurringOrderHubDetails? hub;
|
||||||
|
|
||||||
|
/// Optional order name.
|
||||||
|
final String? eventName;
|
||||||
|
|
||||||
|
/// Selected vendor id for this order.
|
||||||
|
final String? vendorId;
|
||||||
|
|
||||||
|
/// Role hourly rates keyed by role id.
|
||||||
|
final Map<String, double> roleRates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
recurringDays,
|
||||||
|
location,
|
||||||
|
positions,
|
||||||
|
hub,
|
||||||
|
eventName,
|
||||||
|
vendorId,
|
||||||
|
roleRates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal hub details used during recurring order creation.
|
||||||
|
class RecurringOrderHubDetails extends Equatable {
|
||||||
|
const RecurringOrderHubDetails({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String address;
|
||||||
|
final String? placeId;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? street;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Represents a specific position requirement within a [RecurringOrder].
|
||||||
|
class RecurringOrderPosition extends Equatable {
|
||||||
|
const RecurringOrderPosition({
|
||||||
|
required this.role,
|
||||||
|
required this.count,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
this.lunchBreak = 'NO_BREAK',
|
||||||
|
this.location,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The job role or title required.
|
||||||
|
final String role;
|
||||||
|
|
||||||
|
/// The number of workers required for this position.
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// The scheduled start time (e.g., "09:00 AM").
|
||||||
|
final String startTime;
|
||||||
|
|
||||||
|
/// The scheduled end time (e.g., "05:00 PM").
|
||||||
|
final String endTime;
|
||||||
|
|
||||||
|
/// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30).
|
||||||
|
final String lunchBreak;
|
||||||
|
|
||||||
|
/// Optional specific location for this position, if different from the order's main location.
|
||||||
|
final String? location;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
role,
|
||||||
|
count,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
lunchBreak,
|
||||||
|
location,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Creates a copy of this position with the given fields replaced.
|
||||||
|
RecurringOrderPosition copyWith({
|
||||||
|
String? role,
|
||||||
|
int? count,
|
||||||
|
String? startTime,
|
||||||
|
String? endTime,
|
||||||
|
String? lunchBreak,
|
||||||
|
String? location,
|
||||||
|
}) {
|
||||||
|
return RecurringOrderPosition(
|
||||||
|
role: role ?? this.role,
|
||||||
|
count: count ?? this.count,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
endTime: endTime ?? this.endTime,
|
||||||
|
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||||
|
location: location ?? this.location,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,12 @@ import 'package:krow_data_connect/krow_data_connect.dart';
|
|||||||
import 'data/repositories_impl/client_create_order_repository_impl.dart';
|
import 'data/repositories_impl/client_create_order_repository_impl.dart';
|
||||||
import 'domain/repositories/client_create_order_repository_interface.dart';
|
import 'domain/repositories/client_create_order_repository_interface.dart';
|
||||||
import 'domain/usecases/create_one_time_order_usecase.dart';
|
import 'domain/usecases/create_one_time_order_usecase.dart';
|
||||||
|
import 'domain/usecases/create_recurring_order_usecase.dart';
|
||||||
import 'domain/usecases/create_rapid_order_usecase.dart';
|
import 'domain/usecases/create_rapid_order_usecase.dart';
|
||||||
import 'domain/usecases/get_order_types_usecase.dart';
|
import 'domain/usecases/get_order_types_usecase.dart';
|
||||||
import 'presentation/blocs/client_create_order_bloc.dart';
|
import 'presentation/blocs/client_create_order_bloc.dart';
|
||||||
import 'presentation/blocs/one_time_order_bloc.dart';
|
import 'presentation/blocs/one_time_order_bloc.dart';
|
||||||
|
import 'presentation/blocs/recurring_order_bloc.dart';
|
||||||
import 'presentation/blocs/rapid_order_bloc.dart';
|
import 'presentation/blocs/rapid_order_bloc.dart';
|
||||||
import 'presentation/pages/create_order_page.dart';
|
import 'presentation/pages/create_order_page.dart';
|
||||||
import 'presentation/pages/one_time_order_page.dart';
|
import 'presentation/pages/one_time_order_page.dart';
|
||||||
@@ -33,12 +35,14 @@ class ClientCreateOrderModule extends Module {
|
|||||||
// UseCases
|
// UseCases
|
||||||
i.addLazySingleton(GetOrderTypesUseCase.new);
|
i.addLazySingleton(GetOrderTypesUseCase.new);
|
||||||
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
|
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
|
||||||
|
i.addLazySingleton(CreateRecurringOrderUseCase.new);
|
||||||
i.addLazySingleton(CreateRapidOrderUseCase.new);
|
i.addLazySingleton(CreateRapidOrderUseCase.new);
|
||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
|
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
|
||||||
i.add<RapidOrderBloc>(RapidOrderBloc.new);
|
i.add<RapidOrderBloc>(RapidOrderBloc.new);
|
||||||
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
|
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
|
||||||
|
i.add<RecurringOrderBloc>(RecurringOrderBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
// titleKey: 'client_create_order.types.rapid',
|
// titleKey: 'client_create_order.types.rapid',
|
||||||
// descriptionKey: 'client_create_order.types.rapid_desc',
|
// descriptionKey: 'client_create_order.types.rapid_desc',
|
||||||
// ),
|
// ),
|
||||||
// domain.OrderType(
|
domain.OrderType(
|
||||||
// id: 'recurring',
|
id: 'recurring',
|
||||||
// titleKey: 'client_create_order.types.recurring',
|
titleKey: 'client_create_order.types.recurring',
|
||||||
// descriptionKey: 'client_create_order.types.recurring_desc',
|
descriptionKey: 'client_create_order.types.recurring_desc',
|
||||||
// ),
|
),
|
||||||
// domain.OrderType(
|
// domain.OrderType(
|
||||||
// id: 'permanent',
|
// id: 'permanent',
|
||||||
// titleKey: 'client_create_order.types.permanent',
|
// titleKey: 'client_create_order.types.permanent',
|
||||||
@@ -139,6 +139,105 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||||
|
final fdc.Timestamp startTimestamp = orderTimestamp;
|
||||||
|
final fdc.Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
||||||
|
|
||||||
|
final fdc.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;
|
||||||
|
|
||||||
|
final int workersNeeded = order.positions.fold<int>(
|
||||||
|
0,
|
||||||
|
(int sum, domain.RecurringOrderPosition position) => sum + position.count,
|
||||||
|
);
|
||||||
|
final String shiftTitle = 'Shift 1 ${_formatDate(order.startDate)}';
|
||||||
|
final double shiftCost = _calculateRecurringShiftCost(order);
|
||||||
|
|
||||||
|
final fdc.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.PENDING)
|
||||||
|
.workersNeeded(workersNeeded)
|
||||||
|
.filled(0)
|
||||||
|
.durationDays(1)
|
||||||
|
.cost(shiftCost)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
final String shiftId = shiftResult.data.shift_insert.id;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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(fdc.AnyValue(<String>[shiftId]))
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> createRapidOrder(String description) async {
|
Future<void> createRapidOrder(String description) async {
|
||||||
// TO-DO: connect IA and return array with the information.
|
// TO-DO: connect IA and return array with the information.
|
||||||
@@ -159,6 +258,20 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
return total;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
dc.BreakDuration _breakDurationFromValue(String value) {
|
dc.BreakDuration _breakDurationFromValue(String value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'MIN_10':
|
case 'MIN_10':
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
class RecurringOrderArguments {
|
||||||
|
const RecurringOrderArguments({required this.order});
|
||||||
|
final RecurringOrder order;
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ abstract interface class ClientCreateOrderRepositoryInterface {
|
|||||||
/// [order] contains the date, location, and required positions.
|
/// [order] contains the date, location, and required positions.
|
||||||
Future<void> createOneTimeOrder(OneTimeOrder order);
|
Future<void> createOneTimeOrder(OneTimeOrder order);
|
||||||
|
|
||||||
|
/// Submits a recurring staffing order with specific details.
|
||||||
|
Future<void> createRecurringOrder(RecurringOrder order);
|
||||||
|
|
||||||
/// Submits a rapid (urgent) staffing order via a text description.
|
/// Submits a rapid (urgent) staffing order via a text description.
|
||||||
///
|
///
|
||||||
/// [description] is the text message (or transcribed voice) describing the need.
|
/// [description] is the text message (or transcribed voice) describing the need.
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:krow_core/core.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<RecurringOrderArguments, void> {
|
||||||
|
/// Creates a [CreateRecurringOrderUseCase].
|
||||||
|
const CreateRecurringOrderUseCase(this._repository);
|
||||||
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> call(RecurringOrderArguments input) {
|
||||||
|
return _repository.createRecurringOrder(input.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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 '../../domain/arguments/recurring_order_arguments.dart';
|
||||||
|
import '../../domain/usecases/create_recurring_order_usecase.dart';
|
||||||
|
import 'recurring_order_event.dart';
|
||||||
|
import 'recurring_order_state.dart';
|
||||||
|
|
||||||
|
/// BLoC for managing the recurring order creation form.
|
||||||
|
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||||
|
with BlocErrorHandler<RecurringOrderState>, SafeBloc<RecurringOrderEvent, RecurringOrderState> {
|
||||||
|
RecurringOrderBloc(this._createRecurringOrderUseCase, this._service)
|
||||||
|
: super(RecurringOrderState.initial()) {
|
||||||
|
on<RecurringOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
|
on<RecurringOrderVendorChanged>(_onVendorChanged);
|
||||||
|
on<RecurringOrderHubsLoaded>(_onHubsLoaded);
|
||||||
|
on<RecurringOrderHubChanged>(_onHubChanged);
|
||||||
|
on<RecurringOrderEventNameChanged>(_onEventNameChanged);
|
||||||
|
on<RecurringOrderStartDateChanged>(_onStartDateChanged);
|
||||||
|
on<RecurringOrderEndDateChanged>(_onEndDateChanged);
|
||||||
|
on<RecurringOrderDayToggled>(_onDayToggled);
|
||||||
|
on<RecurringOrderPositionAdded>(_onPositionAdded);
|
||||||
|
on<RecurringOrderPositionRemoved>(_onPositionRemoved);
|
||||||
|
on<RecurringOrderPositionUpdated>(_onPositionUpdated);
|
||||||
|
on<RecurringOrderSubmitted>(_onSubmitted);
|
||||||
|
|
||||||
|
_loadVendors();
|
||||||
|
_loadHubs();
|
||||||
|
}
|
||||||
|
|
||||||
|
final CreateRecurringOrderUseCase _createRecurringOrderUseCase;
|
||||||
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
|
static const List<String> _dayLabels = <String>[
|
||||||
|
'S',
|
||||||
|
'M',
|
||||||
|
'T',
|
||||||
|
'W',
|
||||||
|
'T',
|
||||||
|
'F',
|
||||||
|
'S',
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _loadVendors() async {
|
||||||
|
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
||||||
|
action: () async {
|
||||||
|
final QueryResult<dc.ListVendorsData, void> result =
|
||||||
|
await _service.connector.listVendors().execute();
|
||||||
|
return result.data.vendors
|
||||||
|
.map(
|
||||||
|
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
||||||
|
id: vendor.id,
|
||||||
|
name: vendor.companyName,
|
||||||
|
rates: const <String, double>{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
onError: (_) => add(const RecurringOrderVendorsLoaded(<domain.Vendor>[])),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (vendors != null) {
|
||||||
|
add(RecurringOrderVendorsLoaded(vendors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRolesForVendor(
|
||||||
|
String vendorId,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) async {
|
||||||
|
final List<RecurringOrderRoleOption>? roles = await handleErrorWithResult(
|
||||||
|
action: () async {
|
||||||
|
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
||||||
|
result = await _service.connector
|
||||||
|
.listRolesByVendorId(vendorId: vendorId)
|
||||||
|
.execute();
|
||||||
|
return result.data.roles
|
||||||
|
.map(
|
||||||
|
(dc.ListRolesByVendorIdRoles role) => RecurringOrderRoleOption(
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
costPerHour: role.costPerHour,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
onError: (_) => emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (roles != null) {
|
||||||
|
emit(state.copyWith(roles: roles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadHubs() async {
|
||||||
|
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
|
||||||
|
action: () async {
|
||||||
|
final String businessId = await _service.getBusinessId();
|
||||||
|
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
||||||
|
result = await _service.connector
|
||||||
|
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||||
|
.execute();
|
||||||
|
return result.data.teamHubs
|
||||||
|
.map(
|
||||||
|
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => RecurringOrderHubOption(
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
onError: (_) => add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hubs != null) {
|
||||||
|
add(RecurringOrderHubsLoaded(hubs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onVendorsLoaded(
|
||||||
|
RecurringOrderVendorsLoaded event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) async {
|
||||||
|
final domain.Vendor? selectedVendor =
|
||||||
|
event.vendors.isNotEmpty ? event.vendors.first : null;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
vendors: event.vendors,
|
||||||
|
selectedVendor: selectedVendor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (selectedVendor != null) {
|
||||||
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onVendorChanged(
|
||||||
|
RecurringOrderVendorChanged event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(selectedVendor: event.vendor));
|
||||||
|
await _loadRolesForVendor(event.vendor.id, emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHubsLoaded(
|
||||||
|
RecurringOrderHubsLoaded event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
final RecurringOrderHubOption? selectedHub =
|
||||||
|
event.hubs.isNotEmpty ? event.hubs.first : null;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
hubs: event.hubs,
|
||||||
|
selectedHub: selectedHub,
|
||||||
|
location: selectedHub?.name ?? '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHubChanged(
|
||||||
|
RecurringOrderHubChanged event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selectedHub: event.hub,
|
||||||
|
location: event.hub.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEventNameChanged(
|
||||||
|
RecurringOrderEventNameChanged event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(eventName: event.eventName));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStartDateChanged(
|
||||||
|
RecurringOrderStartDateChanged event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
DateTime endDate = state.endDate;
|
||||||
|
if (endDate.isBefore(event.date)) {
|
||||||
|
endDate = event.date;
|
||||||
|
}
|
||||||
|
emit(state.copyWith(startDate: event.date, endDate: endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEndDateChanged(
|
||||||
|
RecurringOrderEndDateChanged event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
DateTime startDate = state.startDate;
|
||||||
|
if (event.date.isBefore(startDate)) {
|
||||||
|
startDate = event.date;
|
||||||
|
}
|
||||||
|
emit(state.copyWith(endDate: event.date, startDate: startDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDayToggled(
|
||||||
|
RecurringOrderDayToggled event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
final List<int> days = List<int>.from(state.recurringDays);
|
||||||
|
if (days.contains(event.dayIndex)) {
|
||||||
|
days.remove(event.dayIndex);
|
||||||
|
} else {
|
||||||
|
days.add(event.dayIndex);
|
||||||
|
days.sort();
|
||||||
|
}
|
||||||
|
emit(state.copyWith(recurringDays: days));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPositionAdded(
|
||||||
|
RecurringOrderPositionAdded event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
final List<RecurringOrderPosition> newPositions =
|
||||||
|
List<RecurringOrderPosition>.from(state.positions)..add(
|
||||||
|
const RecurringOrderPosition(
|
||||||
|
role: '',
|
||||||
|
count: 1,
|
||||||
|
startTime: '09:00',
|
||||||
|
endTime: '17:00',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(state.copyWith(positions: newPositions));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPositionRemoved(
|
||||||
|
RecurringOrderPositionRemoved event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
if (state.positions.length > 1) {
|
||||||
|
final List<RecurringOrderPosition> newPositions =
|
||||||
|
List<RecurringOrderPosition>.from(state.positions)
|
||||||
|
..removeAt(event.index);
|
||||||
|
emit(state.copyWith(positions: newPositions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPositionUpdated(
|
||||||
|
RecurringOrderPositionUpdated event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) {
|
||||||
|
final List<RecurringOrderPosition> newPositions =
|
||||||
|
List<RecurringOrderPosition>.from(state.positions);
|
||||||
|
newPositions[event.index] = event.position;
|
||||||
|
emit(state.copyWith(positions: newPositions));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmitted(
|
||||||
|
RecurringOrderSubmitted event,
|
||||||
|
Emitter<RecurringOrderState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: RecurringOrderStatus.loading));
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
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 domain.OrderMissingHubException();
|
||||||
|
}
|
||||||
|
final List<String> recurringDays = state.recurringDays
|
||||||
|
.map((int index) => _dayLabels[index])
|
||||||
|
.toList();
|
||||||
|
final domain.RecurringOrder order = domain.RecurringOrder(
|
||||||
|
startDate: state.startDate,
|
||||||
|
endDate: state.endDate,
|
||||||
|
recurringDays: 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,
|
||||||
|
roleRates: roleRates,
|
||||||
|
);
|
||||||
|
await _createRecurringOrderUseCase(
|
||||||
|
RecurringOrderArguments(order: order),
|
||||||
|
);
|
||||||
|
emit(state.copyWith(status: RecurringOrderStatus.success));
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: RecurringOrderStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
||||||
|
import 'recurring_order_state.dart';
|
||||||
|
|
||||||
|
abstract class RecurringOrderEvent extends Equatable {
|
||||||
|
const RecurringOrderEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderVendorsLoaded extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderVendorsLoaded(this.vendors);
|
||||||
|
|
||||||
|
final List<Vendor> vendors;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[vendors];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderVendorChanged extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderVendorChanged(this.vendor);
|
||||||
|
|
||||||
|
final Vendor vendor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[vendor];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderHubsLoaded extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderHubsLoaded(this.hubs);
|
||||||
|
|
||||||
|
final List<RecurringOrderHubOption> hubs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[hubs];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderHubChanged extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderHubChanged(this.hub);
|
||||||
|
|
||||||
|
final RecurringOrderHubOption hub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[hub];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderEventNameChanged extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderEventNameChanged(this.eventName);
|
||||||
|
|
||||||
|
final String eventName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[eventName];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderStartDateChanged extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderStartDateChanged(this.date);
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[date];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderEndDateChanged extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderEndDateChanged(this.date);
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[date];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderDayToggled extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderDayToggled(this.dayIndex);
|
||||||
|
|
||||||
|
final int dayIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[dayIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderPositionAdded extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderPositionAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderPositionRemoved extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderPositionRemoved(this.index);
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderPositionUpdated extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderPositionUpdated(this.index, this.position);
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final RecurringOrderPosition position;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[index, position];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderSubmitted extends RecurringOrderEvent {
|
||||||
|
const RecurringOrderSubmitted();
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
enum RecurringOrderStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class RecurringOrderState extends Equatable {
|
||||||
|
const RecurringOrderState({
|
||||||
|
required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
required this.recurringDays,
|
||||||
|
required this.location,
|
||||||
|
required this.eventName,
|
||||||
|
required this.positions,
|
||||||
|
this.status = RecurringOrderStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.vendors = const <Vendor>[],
|
||||||
|
this.selectedVendor,
|
||||||
|
this.hubs = const <RecurringOrderHubOption>[],
|
||||||
|
this.selectedHub,
|
||||||
|
this.roles = const <RecurringOrderRoleOption>[],
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RecurringOrderState.initial() {
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||||
|
return RecurringOrderState(
|
||||||
|
startDate: start,
|
||||||
|
endDate: start.add(const Duration(days: 7)),
|
||||||
|
recurringDays: const <int>[],
|
||||||
|
location: '',
|
||||||
|
eventName: '',
|
||||||
|
positions: const <RecurringOrderPosition>[
|
||||||
|
RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
||||||
|
],
|
||||||
|
vendors: const <Vendor>[],
|
||||||
|
hubs: const <RecurringOrderHubOption>[],
|
||||||
|
roles: const <RecurringOrderRoleOption>[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final DateTime startDate;
|
||||||
|
final DateTime endDate;
|
||||||
|
final List<int> recurringDays;
|
||||||
|
final String location;
|
||||||
|
final String eventName;
|
||||||
|
final List<RecurringOrderPosition> positions;
|
||||||
|
final RecurringOrderStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
final List<Vendor> vendors;
|
||||||
|
final Vendor? selectedVendor;
|
||||||
|
final List<RecurringOrderHubOption> hubs;
|
||||||
|
final RecurringOrderHubOption? selectedHub;
|
||||||
|
final List<RecurringOrderRoleOption> roles;
|
||||||
|
|
||||||
|
RecurringOrderState copyWith({
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
List<int>? recurringDays,
|
||||||
|
String? location,
|
||||||
|
String? eventName,
|
||||||
|
List<RecurringOrderPosition>? positions,
|
||||||
|
RecurringOrderStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
List<Vendor>? vendors,
|
||||||
|
Vendor? selectedVendor,
|
||||||
|
List<RecurringOrderHubOption>? hubs,
|
||||||
|
RecurringOrderHubOption? selectedHub,
|
||||||
|
List<RecurringOrderRoleOption>? roles,
|
||||||
|
}) {
|
||||||
|
return RecurringOrderState(
|
||||||
|
startDate: startDate ?? this.startDate,
|
||||||
|
endDate: endDate ?? this.endDate,
|
||||||
|
recurringDays: recurringDays ?? this.recurringDays,
|
||||||
|
location: location ?? this.location,
|
||||||
|
eventName: eventName ?? this.eventName,
|
||||||
|
positions: positions ?? this.positions,
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
vendors: vendors ?? this.vendors,
|
||||||
|
selectedVendor: selectedVendor ?? this.selectedVendor,
|
||||||
|
hubs: hubs ?? this.hubs,
|
||||||
|
selectedHub: selectedHub ?? this.selectedHub,
|
||||||
|
roles: roles ?? this.roles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isValid {
|
||||||
|
final bool datesValid = !endDate.isBefore(startDate);
|
||||||
|
return eventName.isNotEmpty &&
|
||||||
|
selectedVendor != null &&
|
||||||
|
selectedHub != null &&
|
||||||
|
positions.isNotEmpty &&
|
||||||
|
recurringDays.isNotEmpty &&
|
||||||
|
datesValid &&
|
||||||
|
positions.every(
|
||||||
|
(RecurringOrderPosition p) =>
|
||||||
|
p.role.isNotEmpty &&
|
||||||
|
p.count > 0 &&
|
||||||
|
p.startTime.isNotEmpty &&
|
||||||
|
p.endTime.isNotEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
recurringDays,
|
||||||
|
location,
|
||||||
|
eventName,
|
||||||
|
positions,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
vendors,
|
||||||
|
selectedVendor,
|
||||||
|
hubs,
|
||||||
|
selectedHub,
|
||||||
|
roles,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderHubOption extends Equatable {
|
||||||
|
const RecurringOrderHubOption({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String address;
|
||||||
|
final String? placeId;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? street;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderRoleOption extends Equatable {
|
||||||
|
const RecurringOrderRoleOption({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.costPerHour,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final double costPerHour;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id, name, costPerHour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecurringOrderPosition extends Equatable {
|
||||||
|
const RecurringOrderPosition({
|
||||||
|
required this.role,
|
||||||
|
required this.count,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
this.lunchBreak,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String role;
|
||||||
|
final int count;
|
||||||
|
final String startTime;
|
||||||
|
final String endTime;
|
||||||
|
final String? lunchBreak;
|
||||||
|
|
||||||
|
RecurringOrderPosition copyWith({
|
||||||
|
String? role,
|
||||||
|
int? count,
|
||||||
|
String? startTime,
|
||||||
|
String? endTime,
|
||||||
|
String? lunchBreak,
|
||||||
|
}) {
|
||||||
|
return RecurringOrderPosition(
|
||||||
|
role: role ?? this.role,
|
||||||
|
count: count ?? this.count,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
endTime: endTime ?? this.endTime,
|
||||||
|
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[role, count, startTime, endTime, lunchBreak];
|
||||||
|
}
|
||||||
@@ -1,40 +1,19 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import '../blocs/recurring_order_bloc.dart';
|
||||||
|
import '../widgets/recurring_order/recurring_order_view.dart';
|
||||||
|
|
||||||
/// Recurring Order Page - Ongoing weekly/monthly coverage.
|
/// Page for creating a recurring staffing order.
|
||||||
/// Placeholder for future implementation.
|
|
||||||
class RecurringOrderPage extends StatelessWidget {
|
class RecurringOrderPage extends StatelessWidget {
|
||||||
/// Creates a [RecurringOrderPage].
|
/// Creates a [RecurringOrderPage].
|
||||||
const RecurringOrderPage({super.key});
|
const RecurringOrderPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientCreateOrderRecurringEn labels =
|
return BlocProvider<RecurringOrderBloc>(
|
||||||
t.client_create_order.recurring;
|
create: (BuildContext context) => Modular.get<RecurringOrderBloc>(),
|
||||||
|
child: const RecurringOrderView(),
|
||||||
return Scaffold(
|
|
||||||
appBar: UiAppBar(
|
|
||||||
title: labels.title,
|
|
||||||
onLeadingPressed: () => Modular.to.toClientHome(),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space6),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
labels.subtitle,
|
|
||||||
style: UiTypography.body1r.textSecondary,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A date picker field for the recurring order form.
|
||||||
|
class RecurringOrderDatePicker extends StatefulWidget {
|
||||||
|
/// Creates a [RecurringOrderDatePicker].
|
||||||
|
const RecurringOrderDatePicker({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display above the field.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The currently selected date.
|
||||||
|
final DateTime value;
|
||||||
|
|
||||||
|
/// Callback when a new date is selected.
|
||||||
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecurringOrderDatePicker> createState() =>
|
||||||
|
_RecurringOrderDatePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringOrderDatePickerState extends State<RecurringOrderDatePicker> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(
|
||||||
|
text: DateFormat('yyyy-MM-dd').format(widget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(RecurringOrderDatePicker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
_controller.text = DateFormat('yyyy-MM-dd').format(widget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
readOnly: true,
|
||||||
|
prefixIcon: UiIcons.calendar,
|
||||||
|
onTap: () async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: widget.value,
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
widget.onChanged(picked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A text input for the order name in the recurring order form.
|
||||||
|
class RecurringOrderEventNameInput extends StatefulWidget {
|
||||||
|
const RecurringOrderEventNameInput({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecurringOrderEventNameInput> createState() =>
|
||||||
|
_RecurringOrderEventNameInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringOrderEventNameInputState
|
||||||
|
extends State<RecurringOrderEventNameInput> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(RecurringOrderEventNameInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != _controller.text) {
|
||||||
|
_controller.text = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
hintText: 'Order name',
|
||||||
|
prefixIcon: UiIcons.briefcase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A header widget for the recurring order flow with a colored background.
|
||||||
|
class RecurringOrderHeader extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderHeader].
|
||||||
|
const RecurringOrderHeader({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.onBack,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title of the page.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The subtitle or description.
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
/// Callback when the back button is pressed.
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top + UiConstants.space5,
|
||||||
|
bottom: UiConstants.space5,
|
||||||
|
left: UiConstants.space5,
|
||||||
|
right: UiConstants.space5,
|
||||||
|
),
|
||||||
|
color: UiColors.primary,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onBack,
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
UiIcons.chevronLeft,
|
||||||
|
color: UiColors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.headline3m.copyWith(color: UiColors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../blocs/recurring_order_state.dart';
|
||||||
|
|
||||||
|
/// A card widget for editing a specific position in a recurring order.
|
||||||
|
class RecurringOrderPositionCard extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderPositionCard].
|
||||||
|
const RecurringOrderPositionCard({
|
||||||
|
required this.index,
|
||||||
|
required this.position,
|
||||||
|
required this.isRemovable,
|
||||||
|
required this.onUpdated,
|
||||||
|
required this.onRemoved,
|
||||||
|
required this.positionLabel,
|
||||||
|
required this.roleLabel,
|
||||||
|
required this.workersLabel,
|
||||||
|
required this.startLabel,
|
||||||
|
required this.endLabel,
|
||||||
|
required this.lunchLabel,
|
||||||
|
required this.roles,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The index of the position in the list.
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
/// The position entity data.
|
||||||
|
final RecurringOrderPosition position;
|
||||||
|
|
||||||
|
/// Whether this position can be removed (usually if there's more than one).
|
||||||
|
final bool isRemovable;
|
||||||
|
|
||||||
|
/// Callback when the position data is updated.
|
||||||
|
final ValueChanged<RecurringOrderPosition> onUpdated;
|
||||||
|
|
||||||
|
/// Callback when the position is removed.
|
||||||
|
final VoidCallback onRemoved;
|
||||||
|
|
||||||
|
/// Label for positions (e.g., "Position").
|
||||||
|
final String positionLabel;
|
||||||
|
|
||||||
|
/// Label for the role selection.
|
||||||
|
final String roleLabel;
|
||||||
|
|
||||||
|
/// Label for the worker count.
|
||||||
|
final String workersLabel;
|
||||||
|
|
||||||
|
/// Label for the start time.
|
||||||
|
final String startLabel;
|
||||||
|
|
||||||
|
/// Label for the end time.
|
||||||
|
final String endLabel;
|
||||||
|
|
||||||
|
/// Label for the lunch break.
|
||||||
|
final String lunchLabel;
|
||||||
|
|
||||||
|
/// Available roles for the selected vendor.
|
||||||
|
final List<RecurringOrderRoleOption> roles;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$positionLabel #${index + 1}',
|
||||||
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
|
),
|
||||||
|
if (isRemovable)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onRemoved,
|
||||||
|
child: Text(
|
||||||
|
t.client_create_order.one_time.remove,
|
||||||
|
style: UiTypography.footnote1m.copyWith(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Role (Dropdown)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
|
value: position.role.isEmpty ? null : position.role,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(role: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _buildRoleItems(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Start/End/Workers Row
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
// Start Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: startLabel,
|
||||||
|
value: position.startTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(startTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// End Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: endLabel,
|
||||||
|
value: position.endTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// Workers Count
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
workersLabel,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (position.count > 1) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${position.count}',
|
||||||
|
style: UiTypography.body2b.textPrimary,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Lunch Break
|
||||||
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: position.lunchBreak,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: <String>[
|
||||||
|
'NO_BREAK',
|
||||||
|
'MIN_10',
|
||||||
|
'MIN_15',
|
||||||
|
'MIN_30',
|
||||||
|
'MIN_45',
|
||||||
|
'MIN_60',
|
||||||
|
].map((String value) {
|
||||||
|
final String label = switch (value) {
|
||||||
|
'NO_BREAK' => 'No Break',
|
||||||
|
'MIN_10' => '10 min (Paid)',
|
||||||
|
'MIN_15' => '15 min (Paid)',
|
||||||
|
'MIN_30' => '30 min (Unpaid)',
|
||||||
|
'MIN_45' => '45 min (Unpaid)',
|
||||||
|
'MIN_60' => '60 min (Unpaid)',
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeInput({
|
||||||
|
required BuildContext context,
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
value.isEmpty ? '--:--' : value,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuItem<String>> _buildRoleItems() {
|
||||||
|
final List<DropdownMenuItem<String>> items = roles
|
||||||
|
.map(
|
||||||
|
(RecurringOrderRoleOption role) => DropdownMenuItem<String>(
|
||||||
|
value: role.id,
|
||||||
|
child: Text(
|
||||||
|
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final bool hasSelected = roles.any((RecurringOrderRoleOption role) => role.id == position.role);
|
||||||
|
if (position.role.isNotEmpty && !hasSelected) {
|
||||||
|
items.add(
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: position.role,
|
||||||
|
child: Text(
|
||||||
|
position.role,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A header widget for sections in the recurring order form.
|
||||||
|
class RecurringOrderSectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderSectionHeader].
|
||||||
|
const RecurringOrderSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title text for the section.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional label for an action button on the right.
|
||||||
|
final String? actionLabel;
|
||||||
|
|
||||||
|
/// Callback when the action button is tapped.
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
|
if (actionLabel != null && onAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.add, size: 16, color: UiColors.primary),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
actionLabel!,
|
||||||
|
style: UiTypography.body2m.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A view to display when a recurring order has been successfully created.
|
||||||
|
class RecurringOrderSuccessView extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderSuccessView].
|
||||||
|
const RecurringOrderSuccessView({
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.buttonLabel,
|
||||||
|
required this.onDone,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title of the success message.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The body of the success message.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Label for the completion button.
|
||||||
|
final String buttonLabel;
|
||||||
|
|
||||||
|
/// Callback when the completion button is tapped.
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg * 1.5,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, UiConstants.space2 + 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: UiConstants.space16,
|
||||||
|
height: UiConstants.space16,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
color: UiColors.black,
|
||||||
|
size: UiConstants.space8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.headline2m.textPrimary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body2r.textSecondary.copyWith(
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: buttonLabel,
|
||||||
|
onPressed: onDone,
|
||||||
|
size: UiButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart' show Vendor;
|
||||||
|
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:krow_core/core.dart';
|
||||||
|
import '../../blocs/recurring_order_bloc.dart';
|
||||||
|
import '../../blocs/recurring_order_event.dart';
|
||||||
|
import '../../blocs/recurring_order_state.dart';
|
||||||
|
import 'recurring_order_date_picker.dart';
|
||||||
|
import 'recurring_order_event_name_input.dart';
|
||||||
|
import 'recurring_order_header.dart';
|
||||||
|
import 'recurring_order_position_card.dart';
|
||||||
|
import 'recurring_order_section_header.dart';
|
||||||
|
import 'recurring_order_success_view.dart';
|
||||||
|
|
||||||
|
/// The main content of the Recurring Order page.
|
||||||
|
class RecurringOrderView extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderView].
|
||||||
|
const RecurringOrderView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCreateOrderRecurringEn labels =
|
||||||
|
t.client_create_order.recurring;
|
||||||
|
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
|
||||||
|
t.client_create_order.one_time;
|
||||||
|
|
||||||
|
return BlocConsumer<RecurringOrderBloc, RecurringOrderState>(
|
||||||
|
listener: (BuildContext context, RecurringOrderState state) {
|
||||||
|
if (state.status == RecurringOrderStatus.failure &&
|
||||||
|
state.errorMessage != null) {
|
||||||
|
final String message = state.errorMessage == 'placeholder'
|
||||||
|
? labels.placeholder
|
||||||
|
: translateErrorKey(state.errorMessage!);
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: message,
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (BuildContext context, RecurringOrderState state) {
|
||||||
|
if (state.status == RecurringOrderStatus.success) {
|
||||||
|
return RecurringOrderSuccessView(
|
||||||
|
title: labels.title,
|
||||||
|
message: labels.subtitle,
|
||||||
|
buttonLabel: oneTimeLabels.back_to_orders,
|
||||||
|
onDone: () => Modular.to.pushNamedAndRemoveUntil(
|
||||||
|
ClientPaths.orders,
|
||||||
|
(_) => false,
|
||||||
|
arguments: <String, dynamic>{
|
||||||
|
'initialDate': state.startDate.toIso8601String(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.vendors.isEmpty &&
|
||||||
|
state.status != RecurringOrderStatus.loading) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
RecurringOrderHeader(
|
||||||
|
title: labels.title,
|
||||||
|
subtitle: labels.subtitle,
|
||||||
|
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.search,
|
||||||
|
size: 64,
|
||||||
|
color: UiColors.iconInactive,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Text(
|
||||||
|
'No Vendors Available',
|
||||||
|
style: UiTypography.headline3m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
'There are no staffing vendors associated with your account.',
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
RecurringOrderHeader(
|
||||||
|
title: labels.title,
|
||||||
|
subtitle: labels.subtitle,
|
||||||
|
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
_RecurringOrderForm(state: state),
|
||||||
|
if (state.status == RecurringOrderStatus.loading)
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_BottomActionButton(
|
||||||
|
label: state.status == RecurringOrderStatus.loading
|
||||||
|
? oneTimeLabels.creating
|
||||||
|
: oneTimeLabels.create_order,
|
||||||
|
isLoading: state.status == RecurringOrderStatus.loading,
|
||||||
|
onPressed: state.isValid
|
||||||
|
? () => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(const RecurringOrderSubmitted())
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringOrderForm extends StatelessWidget {
|
||||||
|
const _RecurringOrderForm({required this.state});
|
||||||
|
final RecurringOrderState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCreateOrderRecurringEn labels =
|
||||||
|
t.client_create_order.recurring;
|
||||||
|
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
|
||||||
|
t.client_create_order.one_time;
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
labels.title,
|
||||||
|
style: UiTypography.headline3m.textPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
RecurringOrderEventNameInput(
|
||||||
|
label: 'ORDER NAME',
|
||||||
|
value: state.eventName,
|
||||||
|
onChanged: (String value) => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderEventNameChanged(value)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Vendor Selection
|
||||||
|
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<Vendor>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: state.selectedVendor,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (Vendor? vendor) {
|
||||||
|
if (vendor != null) {
|
||||||
|
BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderVendorChanged(vendor));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: state.vendors.map((Vendor vendor) {
|
||||||
|
return DropdownMenuItem<Vendor>(
|
||||||
|
value: vendor,
|
||||||
|
child: Text(
|
||||||
|
vendor.name,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
RecurringOrderDatePicker(
|
||||||
|
label: 'Start Date',
|
||||||
|
value: state.startDate,
|
||||||
|
onChanged: (DateTime date) => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderStartDateChanged(date)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
RecurringOrderDatePicker(
|
||||||
|
label: 'End Date',
|
||||||
|
value: state.endDate,
|
||||||
|
onChanged: (DateTime date) => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderEndDateChanged(date)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
Text('Recurring Days', style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
_RecurringDaysSelector(
|
||||||
|
selectedDays: state.recurringDays,
|
||||||
|
onToggle: (int dayIndex) => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderDayToggled(dayIndex)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
Text('HUB', style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<RecurringOrderHubOption>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: state.selectedHub,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (RecurringOrderHubOption? hub) {
|
||||||
|
if (hub != null) {
|
||||||
|
BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderHubChanged(hub));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: state.hubs.map((RecurringOrderHubOption hub) {
|
||||||
|
return DropdownMenuItem<RecurringOrderHubOption>(
|
||||||
|
value: hub,
|
||||||
|
child: Text(
|
||||||
|
hub.name,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
|
RecurringOrderSectionHeader(
|
||||||
|
title: oneTimeLabels.positions_title,
|
||||||
|
actionLabel: oneTimeLabels.add_position,
|
||||||
|
onAction: () => BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(const RecurringOrderPositionAdded()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Positions List
|
||||||
|
...state.positions.asMap().entries.map((
|
||||||
|
MapEntry<int, RecurringOrderPosition> entry,
|
||||||
|
) {
|
||||||
|
final int index = entry.key;
|
||||||
|
final RecurringOrderPosition position = entry.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: RecurringOrderPositionCard(
|
||||||
|
index: index,
|
||||||
|
position: position,
|
||||||
|
isRemovable: state.positions.length > 1,
|
||||||
|
positionLabel: oneTimeLabels.positions_title,
|
||||||
|
roleLabel: oneTimeLabels.select_role,
|
||||||
|
workersLabel: oneTimeLabels.workers_label,
|
||||||
|
startLabel: oneTimeLabels.start_label,
|
||||||
|
endLabel: oneTimeLabels.end_label,
|
||||||
|
lunchLabel: oneTimeLabels.lunch_break_label,
|
||||||
|
roles: state.roles,
|
||||||
|
onUpdated: (RecurringOrderPosition updated) {
|
||||||
|
BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderPositionUpdated(index, updated));
|
||||||
|
},
|
||||||
|
onRemoved: () {
|
||||||
|
BlocProvider.of<RecurringOrderBloc>(
|
||||||
|
context,
|
||||||
|
).add(RecurringOrderPositionRemoved(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringDaysSelector extends StatelessWidget {
|
||||||
|
const _RecurringDaysSelector({
|
||||||
|
required this.selectedDays,
|
||||||
|
required this.onToggle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<int> selectedDays;
|
||||||
|
final ValueChanged<int> onToggle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const List<String> labels = <String>['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||||
|
return Wrap(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
children: List<Widget>.generate(labels.length, (int index) {
|
||||||
|
final bool isSelected = selectedDays.contains(index);
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onToggle(index),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
labels[index],
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: isSelected ? UiColors.white : UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomActionButton extends StatelessWidget {
|
||||||
|
const _BottomActionButton({
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.isLoading = false,
|
||||||
|
});
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: UiConstants.space5,
|
||||||
|
right: UiConstants.space5,
|
||||||
|
top: UiConstants.space5,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: label,
|
||||||
|
onPressed: isLoading ? null : onPressed,
|
||||||
|
size: UiButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ mutation createOrder(
|
|||||||
$shifts: Any
|
$shifts: Any
|
||||||
$requested: Int
|
$requested: Int
|
||||||
$teamHubId: UUID!
|
$teamHubId: UUID!
|
||||||
$recurringDays: Any
|
$recurringDays: [String!]
|
||||||
$permanentStartDate: Timestamp
|
$permanentStartDate: Timestamp
|
||||||
$permanentDays: Any
|
$permanentDays: Any
|
||||||
$notes: String
|
$notes: String
|
||||||
@@ -64,7 +64,7 @@ mutation updateOrder(
|
|||||||
$shifts: Any
|
$shifts: Any
|
||||||
$requested: Int
|
$requested: Int
|
||||||
$teamHubId: UUID!
|
$teamHubId: UUID!
|
||||||
$recurringDays: Any
|
$recurringDays: [String!]
|
||||||
$permanentDays: Any
|
$permanentDays: Any
|
||||||
$notes: String
|
$notes: String
|
||||||
$detectedConflicts: Any
|
$detectedConflicts: Any
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
specVersion: "v1"
|
specVersion: "v1"
|
||||||
serviceId: "krow-workforce-db-validation"
|
serviceId: "krow-workforce-db"
|
||||||
location: "us-central1"
|
location: "us-central1"
|
||||||
schema:
|
schema:
|
||||||
source: "./schema"
|
source: "./schema"
|
||||||
@@ -7,7 +7,7 @@ schema:
|
|||||||
postgresql:
|
postgresql:
|
||||||
database: "krow_db"
|
database: "krow_db"
|
||||||
cloudSql:
|
cloudSql:
|
||||||
instanceId: "krow-sql-validation"
|
instanceId: "krow-sql"
|
||||||
# schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly.
|
# schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly.
|
||||||
# schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect.
|
# schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect.
|
||||||
connectorDirs: ["./connector"]
|
connectorDirs: ["./connector"]
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type Order @table(name: "orders", key: ["id"]) {
|
|||||||
startDate: Timestamp #for recurring and permanent
|
startDate: Timestamp #for recurring and permanent
|
||||||
endDate: Timestamp #for recurring and permanent
|
endDate: Timestamp #for recurring and permanent
|
||||||
|
|
||||||
recurringDays: Any @col(dataType: "jsonb")
|
recurringDays: [String!]
|
||||||
poReference: String
|
poReference: String
|
||||||
|
|
||||||
permanentDays: Any @col(dataType: "jsonb")
|
permanentDays: Any @col(dataType: "jsonb")
|
||||||
|
|||||||
Reference in New Issue
Block a user