feat(view_orders): implement View Orders feature with filter tabs and calendar navigation

- Added ViewOrdersFilterTab widget for displaying filter options with counts.
- Created ViewOrdersHeader widget to include a sticky header with title, filter tabs, and calendar controls.
- Established ViewOrdersModule for dependency injection of repositories, use cases, and BLoCs.
- Integrated ViewOrdersPage to handle initial date arguments for displaying orders.
- Updated pubspec.yaml with necessary dependencies for the View Orders feature.
This commit is contained in:
Achintha Isuru
2026-02-21 19:11:18 -05:00
parent 8a71f98deb
commit b6f4d656dc
214 changed files with 4664 additions and 12 deletions

View File

@@ -0,0 +1,4 @@
/// Library for the Client Create Order feature.
library;
export 'src/create_order_module.dart';

View File

@@ -0,0 +1,68 @@
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 'data/repositories_impl/client_create_order_repository_impl.dart';
import 'domain/repositories/client_create_order_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 'presentation/blocs/index.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
import 'presentation/pages/permanent_order_page.dart';
import 'presentation/pages/rapid_order_page.dart';
import 'presentation/pages/recurring_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.
class ClientCreateOrderModule extends Module {
@override
List<Module> get imports => <Module>[DataConnectModule()];
@override
void binds(Injector i) {
// Repositories
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
// UseCases
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
i.addLazySingleton(CreatePermanentOrderUseCase.new);
i.addLazySingleton(CreateRecurringOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new);
// BLoCs
i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
i.add<RecurringOrderBloc>(RecurringOrderBloc.new);
}
@override
void routes(RouteManager r) {
r.child(
'/',
child: (BuildContext context) => const ClientCreateOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid),
child: (BuildContext context) => const RapidOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime),
child: (BuildContext context) => const OneTimeOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring),
child: (BuildContext context) => const RecurringOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent),
child: (BuildContext context) => const PermanentOrderPage(),
);
}
}

View File

@@ -0,0 +1,476 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
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 '../../domain/repositories/client_create_order_repository_interface.dart';
/// 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.
class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({
required dc.DataConnectService service,
}) : _service = service;
final dc.DataConnectService _service;
@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 fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.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 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.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(fdc.AnyValue(<String>[shiftId]))
.execute();
});
}
@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;
// 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 fdc.Timestamp dayTimestamp = _service.toTimestamp(
DateTime(day.year, day.month, day.day),
);
final fdc.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(fdc.AnyValue(shiftIds))
.execute();
});
}
@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 fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.Timestamp startTimestamp = orderTimestamp;
final fdc.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 fdc.Timestamp dayTimestamp = _service.toTimestamp(
DateTime(day.year, day.month, day.day),
);
final fdc.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(fdc.AnyValue(shiftIds))
.execute();
});
}
@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<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.');
}
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,
);
}
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';
}
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
///
/// Encapsulates the [OneTimeOrder] details required to create a new
/// one-time staffing request.
class OneTimeOrderArguments extends UseCaseArgument {
/// Creates a [OneTimeOrderArguments] instance.
///
/// Requires the [order] details.
const OneTimeOrderArguments({required this.order});
/// The order details to be created.
final OneTimeOrder order;
@override
List<Object?> get props => <Object?>[order];
}

View File

@@ -0,0 +1,6 @@
import 'package:krow_domain/krow_domain.dart';
class PermanentOrderArguments {
const PermanentOrderArguments({required this.order});
final PermanentOrder order;
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the [CreateRapidOrderUseCase].
///
/// Encapsulates the text description of the urgent staffing need
/// for rapid order creation.
class RapidOrderArguments extends UseCaseArgument {
/// Creates a [RapidOrderArguments] instance.
///
/// Requires the [description] of the staffing need.
const RapidOrderArguments({required this.description});
/// The text description of the urgent staffing need.
final String description;
@override
List<Object?> get props => <Object?>[description];
}

View File

@@ -0,0 +1,6 @@
import 'package:krow_domain/krow_domain.dart';
class RecurringOrderArguments {
const RecurringOrderArguments({required this.order});
final RecurringOrder order;
}

View File

@@ -0,0 +1,32 @@
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.
abstract interface class ClientCreateOrderRepositoryInterface {
/// Submits a one-time staffing order with specific details.
///
/// [order] contains the date, location, and required positions.
Future<void> createOneTimeOrder(OneTimeOrder order);
/// Submits a recurring staffing order with specific details.
Future<void> createRecurringOrder(RecurringOrder order);
/// Submits a permanent staffing order with specific details.
Future<void> createPermanentOrder(PermanentOrder order);
/// 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);
/// 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);
}

View File

@@ -0,0 +1,22 @@
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].
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);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a permanent staffing order.
class CreatePermanentOrderUseCase implements UseCase<PermanentOrder, void> {
const CreatePermanentOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<void> call(PermanentOrder params) {
return _repository.createPermanentOrder(params);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:krow_core/core.dart';
import '../arguments/rapid_order_arguments.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a rapid (urgent) staffing order.
///
/// This use case handles urgent, text-based staffing requests and
/// delegates the submission to the [ClientCreateOrderRepositoryInterface].
class CreateRapidOrderUseCase implements UseCase<RapidOrderArguments, void> {
/// Creates a [CreateRapidOrderUseCase].
///
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
const CreateRapidOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<void> call(RapidOrderArguments input) {
return _repository.createRapidOrder(input.description);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a recurring staffing order.
class CreateRecurringOrderUseCase implements UseCase<RecurringOrder, void> {
const CreateRecurringOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<void> call(RecurringOrder params) {
return _repository.createRecurringOrder(params);
}
}

View File

@@ -0,0 +1,25 @@
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);
}
}

View File

@@ -0,0 +1,4 @@
export 'one_time_order/index.dart';
export 'rapid_order/index.dart';
export 'recurring_order/index.dart';
export 'permanent_order/index.dart';

View File

@@ -0,0 +1,3 @@
export 'one_time_order_bloc.dart';
export 'one_time_order_event.dart';
export 'one_time_order_state.dart';

View File

@@ -0,0 +1,264 @@
import 'package:client_create_order/src/domain/arguments/one_time_order_arguments.dart';
import 'package:client_create_order/src/domain/usecases/create_one_time_order_usecase.dart';
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';
import 'one_time_order_event.dart';
import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service)
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
on<OneTimeOrderVendorChanged>(_onVendorChanged);
on<OneTimeOrderHubsLoaded>(_onHubsLoaded);
on<OneTimeOrderHubChanged>(_onHubChanged);
on<OneTimeOrderEventNameChanged>(_onEventNameChanged);
on<OneTimeOrderDateChanged>(_onDateChanged);
on<OneTimeOrderPositionAdded>(_onPositionAdded);
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
on<OneTimeOrderSubmitted>(_onSubmitted);
_loadVendors();
_loadHubs();
}
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
final dc.DataConnectService _service;
Future<void> _loadVendors() async {
final List<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) => Vendor(
id: vendor.id,
name: vendor.companyName,
rates: const <String, double>{},
),
)
.toList();
},
onError: (_) => add(const OneTimeOrderVendorsLoaded(<Vendor>[])),
);
if (vendors != null) {
add(OneTimeOrderVendorsLoaded(vendors));
}
}
Future<void> _loadRolesForVendor(String vendorId, Emitter<OneTimeOrderState> emit) async {
final List<OneTimeOrderRoleOption>? 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) => OneTimeOrderRoleOption(
id: role.id,
name: role.name,
costPerHour: role.costPerHour,
),
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
);
if (roles != null) {
emit(state.copyWith(roles: roles));
}
}
Future<void> _loadHubs() async {
final List<OneTimeOrderHubOption>? 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) => OneTimeOrderHubOption(
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 OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
);
if (hubs != null) {
add(OneTimeOrderHubsLoaded(hubs));
}
}
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,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
}
}
Future<void> _onVendorChanged(
OneTimeOrderVendorChanged event,
Emitter<OneTimeOrderState> emit,
) async {
emit(state.copyWith(selectedVendor: event.vendor));
await _loadRolesForVendor(event.vendor.id, emit);
}
void _onHubsLoaded(
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 ?? '',
),
);
}
void _onHubChanged(
OneTimeOrderHubChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(
state.copyWith(
selectedHub: event.hub,
location: event.hub.name,
),
);
}
void _onEventNameChanged(
OneTimeOrderEventNameChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(eventName: event.eventName));
}
void _onDateChanged(
OneTimeOrderDateChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(date: event.date));
}
void _onPositionAdded(
OneTimeOrderPositionAdded event,
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)..add(
const OneTimeOrderPosition(
role: '',
count: 1,
startTime: '09:00',
endTime: '17:00',
),
);
emit(state.copyWith(positions: newPositions));
}
void _onPositionRemoved(
OneTimeOrderPositionRemoved event,
Emitter<OneTimeOrderState> emit,
) {
if (state.positions.length > 1) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)
..removeAt(event.index);
emit(state.copyWith(positions: newPositions));
}
}
void _onPositionUpdated(
OneTimeOrderPositionUpdated event,
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions);
newPositions[event.index] = event.position;
emit(state.copyWith(positions: newPositions));
}
Future<void> _onSubmitted(
OneTimeOrderSubmitted event,
Emitter<OneTimeOrderState> emit,
) async {
emit(state.copyWith(status: OneTimeOrderStatus.loading));
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,
roleRates: roleRates,
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import 'one_time_order_state.dart';
abstract class OneTimeOrderEvent extends Equatable {
const OneTimeOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class OneTimeOrderVendorsLoaded extends OneTimeOrderEvent {
const OneTimeOrderVendorsLoaded(this.vendors);
final List<Vendor> vendors;
@override
List<Object?> get props => <Object?>[vendors];
}
class OneTimeOrderVendorChanged extends OneTimeOrderEvent {
const OneTimeOrderVendorChanged(this.vendor);
final Vendor vendor;
@override
List<Object?> get props => <Object?>[vendor];
}
class OneTimeOrderHubsLoaded extends OneTimeOrderEvent {
const OneTimeOrderHubsLoaded(this.hubs);
final List<OneTimeOrderHubOption> hubs;
@override
List<Object?> get props => <Object?>[hubs];
}
class OneTimeOrderHubChanged extends OneTimeOrderEvent {
const OneTimeOrderHubChanged(this.hub);
final OneTimeOrderHubOption hub;
@override
List<Object?> get props => <Object?>[hub];
}
class OneTimeOrderEventNameChanged extends OneTimeOrderEvent {
const OneTimeOrderEventNameChanged(this.eventName);
final String eventName;
@override
List<Object?> get props => <Object?>[eventName];
}
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
const OneTimeOrderDateChanged(this.date);
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}
class OneTimeOrderPositionAdded extends OneTimeOrderEvent {
const OneTimeOrderPositionAdded();
}
class OneTimeOrderPositionRemoved extends OneTimeOrderEvent {
const OneTimeOrderPositionRemoved(this.index);
final int index;
@override
List<Object?> get props => <Object?>[index];
}
class OneTimeOrderPositionUpdated extends OneTimeOrderEvent {
const OneTimeOrderPositionUpdated(this.index, this.position);
final int index;
final OneTimeOrderPosition position;
@override
List<Object?> get props => <Object?>[index, position];
}
class OneTimeOrderSubmitted extends OneTimeOrderEvent {
const OneTimeOrderSubmitted();
}

View File

@@ -0,0 +1,160 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum OneTimeOrderStatus { initial, loading, success, failure }
class OneTimeOrderState extends Equatable {
const OneTimeOrderState({
required this.date,
required this.location,
required this.eventName,
required this.positions,
this.status = OneTimeOrderStatus.initial,
this.errorMessage,
this.vendors = const <Vendor>[],
this.selectedVendor,
this.hubs = const <OneTimeOrderHubOption>[],
this.selectedHub,
this.roles = const <OneTimeOrderRoleOption>[],
});
factory OneTimeOrderState.initial() {
return OneTimeOrderState(
date: DateTime.now(),
location: '',
eventName: '',
positions: const <OneTimeOrderPosition>[
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
vendors: const <Vendor>[],
hubs: const <OneTimeOrderHubOption>[],
roles: const <OneTimeOrderRoleOption>[],
);
}
final DateTime date;
final String location;
final String eventName;
final List<OneTimeOrderPosition> positions;
final OneTimeOrderStatus status;
final String? errorMessage;
final List<Vendor> vendors;
final Vendor? selectedVendor;
final List<OneTimeOrderHubOption> hubs;
final OneTimeOrderHubOption? selectedHub;
final List<OneTimeOrderRoleOption> roles;
OneTimeOrderState copyWith({
DateTime? date,
String? location,
String? eventName,
List<OneTimeOrderPosition>? positions,
OneTimeOrderStatus? status,
String? errorMessage,
List<Vendor>? vendors,
Vendor? selectedVendor,
List<OneTimeOrderHubOption>? hubs,
OneTimeOrderHubOption? selectedHub,
List<OneTimeOrderRoleOption>? roles,
}) {
return OneTimeOrderState(
date: date ?? this.date,
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 {
return eventName.isNotEmpty &&
selectedVendor != null &&
selectedHub != null &&
positions.isNotEmpty &&
positions.every(
(OneTimeOrderPosition p) =>
p.role.isNotEmpty &&
p.count > 0 &&
p.startTime.isNotEmpty &&
p.endTime.isNotEmpty,
);
}
@override
List<Object?> get props => <Object?>[
date,
location,
eventName,
positions,
status,
errorMessage,
vendors,
selectedVendor,
hubs,
selectedHub,
roles,
];
}
class OneTimeOrderHubOption extends Equatable {
const OneTimeOrderHubOption({
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 OneTimeOrderRoleOption extends Equatable {
const OneTimeOrderRoleOption({
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];
}

View File

@@ -0,0 +1,3 @@
export 'permanent_order_bloc.dart';
export 'permanent_order_event.dart';
export 'permanent_order_state.dart';

View File

@@ -0,0 +1,335 @@
import 'package:client_create_order/src/domain/usecases/create_permanent_order_usecase.dart';
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 'permanent_order_event.dart';
import 'permanent_order_state.dart';
/// BLoC for managing the permanent order creation form.
class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
with BlocErrorHandler<PermanentOrderState>, SafeBloc<PermanentOrderEvent, PermanentOrderState> {
PermanentOrderBloc(this._createPermanentOrderUseCase, this._service)
: super(PermanentOrderState.initial()) {
on<PermanentOrderVendorsLoaded>(_onVendorsLoaded);
on<PermanentOrderVendorChanged>(_onVendorChanged);
on<PermanentOrderHubsLoaded>(_onHubsLoaded);
on<PermanentOrderHubChanged>(_onHubChanged);
on<PermanentOrderEventNameChanged>(_onEventNameChanged);
on<PermanentOrderStartDateChanged>(_onStartDateChanged);
on<PermanentOrderDayToggled>(_onDayToggled);
on<PermanentOrderPositionAdded>(_onPositionAdded);
on<PermanentOrderPositionRemoved>(_onPositionRemoved);
on<PermanentOrderPositionUpdated>(_onPositionUpdated);
on<PermanentOrderSubmitted>(_onSubmitted);
_loadVendors();
_loadHubs();
}
final CreatePermanentOrderUseCase _createPermanentOrderUseCase;
final dc.DataConnectService _service;
static const List<String> _dayLabels = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
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 PermanentOrderVendorsLoaded(<domain.Vendor>[])),
);
if (vendors != null) {
add(PermanentOrderVendorsLoaded(vendors));
}
}
Future<void> _loadRolesForVendor(
String vendorId,
Emitter<PermanentOrderState> emit,
) async {
final List<PermanentOrderRoleOption>? 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) => PermanentOrderRoleOption(
id: role.id,
name: role.name,
costPerHour: role.costPerHour,
),
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
);
if (roles != null) {
emit(state.copyWith(roles: roles));
}
}
Future<void> _loadHubs() async {
final List<PermanentOrderHubOption>? 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) => PermanentOrderHubOption(
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 PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
);
if (hubs != null) {
add(PermanentOrderHubsLoaded(hubs));
}
}
Future<void> _onVendorsLoaded(
PermanentOrderVendorsLoaded event,
Emitter<PermanentOrderState> 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(
PermanentOrderVendorChanged event,
Emitter<PermanentOrderState> emit,
) async {
emit(state.copyWith(selectedVendor: event.vendor));
await _loadRolesForVendor(event.vendor.id, emit);
}
void _onHubsLoaded(
PermanentOrderHubsLoaded event,
Emitter<PermanentOrderState> emit,
) {
final PermanentOrderHubOption? selectedHub =
event.hubs.isNotEmpty ? event.hubs.first : null;
emit(
state.copyWith(
hubs: event.hubs,
selectedHub: selectedHub,
location: selectedHub?.name ?? '',
),
);
}
void _onHubChanged(
PermanentOrderHubChanged event,
Emitter<PermanentOrderState> emit,
) {
emit(
state.copyWith(
selectedHub: event.hub,
location: event.hub.name,
),
);
}
void _onEventNameChanged(
PermanentOrderEventNameChanged event,
Emitter<PermanentOrderState> emit,
) {
emit(state.copyWith(eventName: event.eventName));
}
void _onStartDateChanged(
PermanentOrderStartDateChanged event,
Emitter<PermanentOrderState> emit,
) {
final int newDayIndex = event.date.weekday % 7;
final int? autoIndex = state.autoSelectedDayIndex;
List<String> days = List<String>.from(state.permanentDays);
if (autoIndex != null) {
final String oldDay = _dayLabels[autoIndex];
days.remove(oldDay);
final String newDay = _dayLabels[newDayIndex];
if (!days.contains(newDay)) {
days.add(newDay);
}
days = _sortDays(days);
}
emit(
state.copyWith(
startDate: event.date,
permanentDays: days,
autoSelectedDayIndex: autoIndex == null ? null : newDayIndex,
),
);
}
void _onDayToggled(
PermanentOrderDayToggled event,
Emitter<PermanentOrderState> emit,
) {
final List<String> days = List<String>.from(state.permanentDays);
final String label = _dayLabels[event.dayIndex];
int? autoIndex = state.autoSelectedDayIndex;
if (days.contains(label)) {
days.remove(label);
if (autoIndex == event.dayIndex) {
autoIndex = null;
}
} else {
days.add(label);
}
emit(state.copyWith(permanentDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
}
void _onPositionAdded(
PermanentOrderPositionAdded event,
Emitter<PermanentOrderState> emit,
) {
final List<PermanentOrderPosition> newPositions =
List<PermanentOrderPosition>.from(state.positions)..add(
const PermanentOrderPosition(
role: '',
count: 1,
startTime: '09:00',
endTime: '17:00',
),
);
emit(state.copyWith(positions: newPositions));
}
void _onPositionRemoved(
PermanentOrderPositionRemoved event,
Emitter<PermanentOrderState> emit,
) {
if (state.positions.length > 1) {
final List<PermanentOrderPosition> newPositions =
List<PermanentOrderPosition>.from(state.positions)
..removeAt(event.index);
emit(state.copyWith(positions: newPositions));
}
}
void _onPositionUpdated(
PermanentOrderPositionUpdated event,
Emitter<PermanentOrderState> emit,
) {
final List<PermanentOrderPosition> newPositions =
List<PermanentOrderPosition>.from(state.positions);
newPositions[event.index] = event.position;
emit(state.copyWith(positions: newPositions));
}
Future<void> _onSubmitted(
PermanentOrderSubmitted event,
Emitter<PermanentOrderState> emit,
) async {
emit(state.copyWith(status: PermanentOrderStatus.loading));
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,
roleRates: roleRates,
);
await _createPermanentOrderUseCase(order);
emit(state.copyWith(status: PermanentOrderStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: PermanentOrderStatus.failure,
errorMessage: errorKey,
),
);
}
static List<String> _sortDays(List<String> days) {
days.sort(
(String a, String b) =>
_dayLabels.indexOf(a).compareTo(_dayLabels.indexOf(b)),
);
return days;
}
}

View File

@@ -0,0 +1,100 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import 'permanent_order_state.dart';
abstract class PermanentOrderEvent extends Equatable {
const PermanentOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class PermanentOrderVendorsLoaded extends PermanentOrderEvent {
const PermanentOrderVendorsLoaded(this.vendors);
final List<Vendor> vendors;
@override
List<Object?> get props => <Object?>[vendors];
}
class PermanentOrderVendorChanged extends PermanentOrderEvent {
const PermanentOrderVendorChanged(this.vendor);
final Vendor vendor;
@override
List<Object?> get props => <Object?>[vendor];
}
class PermanentOrderHubsLoaded extends PermanentOrderEvent {
const PermanentOrderHubsLoaded(this.hubs);
final List<PermanentOrderHubOption> hubs;
@override
List<Object?> get props => <Object?>[hubs];
}
class PermanentOrderHubChanged extends PermanentOrderEvent {
const PermanentOrderHubChanged(this.hub);
final PermanentOrderHubOption hub;
@override
List<Object?> get props => <Object?>[hub];
}
class PermanentOrderEventNameChanged extends PermanentOrderEvent {
const PermanentOrderEventNameChanged(this.eventName);
final String eventName;
@override
List<Object?> get props => <Object?>[eventName];
}
class PermanentOrderStartDateChanged extends PermanentOrderEvent {
const PermanentOrderStartDateChanged(this.date);
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}
class PermanentOrderDayToggled extends PermanentOrderEvent {
const PermanentOrderDayToggled(this.dayIndex);
final int dayIndex;
@override
List<Object?> get props => <Object?>[dayIndex];
}
class PermanentOrderPositionAdded extends PermanentOrderEvent {
const PermanentOrderPositionAdded();
}
class PermanentOrderPositionRemoved extends PermanentOrderEvent {
const PermanentOrderPositionRemoved(this.index);
final int index;
@override
List<Object?> get props => <Object?>[index];
}
class PermanentOrderPositionUpdated extends PermanentOrderEvent {
const PermanentOrderPositionUpdated(this.index, this.position);
final int index;
final PermanentOrderPosition position;
@override
List<Object?> get props => <Object?>[index, position];
}
class PermanentOrderSubmitted extends PermanentOrderEvent {
const PermanentOrderSubmitted();
}

View File

@@ -0,0 +1,221 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum PermanentOrderStatus { initial, loading, success, failure }
class PermanentOrderState extends Equatable {
const PermanentOrderState({
required this.startDate,
required this.permanentDays,
required this.location,
required this.eventName,
required this.positions,
required this.autoSelectedDayIndex,
this.status = PermanentOrderStatus.initial,
this.errorMessage,
this.vendors = const <Vendor>[],
this.selectedVendor,
this.hubs = const <PermanentOrderHubOption>[],
this.selectedHub,
this.roles = const <PermanentOrderRoleOption>[],
});
factory PermanentOrderState.initial() {
final DateTime now = DateTime.now();
final DateTime start = DateTime(now.year, now.month, now.day);
final List<String> dayLabels = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
final int weekdayIndex = now.weekday % 7;
return PermanentOrderState(
startDate: start,
permanentDays: <String>[dayLabels[weekdayIndex]],
location: '',
eventName: '',
positions: const <PermanentOrderPosition>[
PermanentOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
autoSelectedDayIndex: weekdayIndex,
vendors: const <Vendor>[],
hubs: const <PermanentOrderHubOption>[],
roles: const <PermanentOrderRoleOption>[],
);
}
final DateTime startDate;
final List<String> permanentDays;
final String location;
final String eventName;
final List<PermanentOrderPosition> positions;
final int? autoSelectedDayIndex;
final PermanentOrderStatus status;
final String? errorMessage;
final List<Vendor> vendors;
final Vendor? selectedVendor;
final List<PermanentOrderHubOption> hubs;
final PermanentOrderHubOption? selectedHub;
final List<PermanentOrderRoleOption> roles;
PermanentOrderState copyWith({
DateTime? startDate,
List<String>? permanentDays,
String? location,
String? eventName,
List<PermanentOrderPosition>? positions,
int? autoSelectedDayIndex,
PermanentOrderStatus? status,
String? errorMessage,
List<Vendor>? vendors,
Vendor? selectedVendor,
List<PermanentOrderHubOption>? hubs,
PermanentOrderHubOption? selectedHub,
List<PermanentOrderRoleOption>? roles,
}) {
return PermanentOrderState(
startDate: startDate ?? this.startDate,
permanentDays: permanentDays ?? this.permanentDays,
location: location ?? this.location,
eventName: eventName ?? this.eventName,
positions: positions ?? this.positions,
autoSelectedDayIndex: autoSelectedDayIndex ?? this.autoSelectedDayIndex,
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 {
return eventName.isNotEmpty &&
selectedVendor != null &&
selectedHub != null &&
positions.isNotEmpty &&
permanentDays.isNotEmpty &&
positions.every(
(PermanentOrderPosition p) =>
p.role.isNotEmpty &&
p.count > 0 &&
p.startTime.isNotEmpty &&
p.endTime.isNotEmpty,
);
}
@override
List<Object?> get props => <Object?>[
startDate,
permanentDays,
location,
eventName,
positions,
autoSelectedDayIndex,
status,
errorMessage,
vendors,
selectedVendor,
hubs,
selectedHub,
roles,
];
}
class PermanentOrderHubOption extends Equatable {
const PermanentOrderHubOption({
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 PermanentOrderRoleOption extends Equatable {
const PermanentOrderRoleOption({
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 PermanentOrderPosition extends Equatable {
const PermanentOrderPosition({
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;
PermanentOrderPosition copyWith({
String? role,
int? count,
String? startTime,
String? endTime,
String? lunchBreak,
}) {
return PermanentOrderPosition(
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];
}

View File

@@ -0,0 +1,3 @@
export 'rapid_order_bloc.dart';
export 'rapid_order_event.dart';
export 'rapid_order_state.dart';

View File

@@ -0,0 +1,95 @@
import 'package:client_create_order/src/domain/arguments/rapid_order_arguments.dart';
import 'package:client_create_order/src/domain/usecases/create_rapid_order_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'rapid_order_event.dart';
import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
with BlocErrorHandler<RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
const RapidOrderInitial(
examples: <String>[
'"We had a call out. Need 2 cooks ASAP"',
'"Need 5 bartenders ASAP until 5am"',
'"Emergency! Need 3 servers right now till midnight"',
],
),
) {
on<RapidOrderMessageChanged>(_onMessageChanged);
on<RapidOrderVoiceToggled>(_onVoiceToggled);
on<RapidOrderSubmitted>(_onSubmitted);
on<RapidOrderExampleSelected>(_onExampleSelected);
}
final CreateRapidOrderUseCase _createRapidOrderUseCase;
void _onMessageChanged(
RapidOrderMessageChanged event,
Emitter<RapidOrderState> emit,
) {
if (state is RapidOrderInitial) {
emit((state as RapidOrderInitial).copyWith(message: event.message));
}
}
Future<void> _onVoiceToggled(
RapidOrderVoiceToggled event,
Emitter<RapidOrderState> emit,
) async {
if (state is RapidOrderInitial) {
final RapidOrderInitial currentState = state as RapidOrderInitial;
final bool newListeningState = !currentState.isListening;
emit(currentState.copyWith(isListening: newListeningState));
// Simulate voice recognition
if (newListeningState) {
await Future<void>.delayed(const Duration(seconds: 2));
if (state is RapidOrderInitial) {
emit(
(state as RapidOrderInitial).copyWith(
message: 'Need 2 servers for a banquet right now.',
isListening: false,
),
);
}
}
}
}
Future<void> _onSubmitted(
RapidOrderSubmitted event,
Emitter<RapidOrderState> emit,
) async {
final RapidOrderState currentState = state;
if (currentState is RapidOrderInitial) {
final String message = currentState.message;
emit(const RapidOrderSubmitting());
await handleError(
emit: emit.call,
action: () async {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message),
);
emit(const RapidOrderSuccess());
},
onError: (String errorKey) => RapidOrderFailure(errorKey),
);
}
}
void _onExampleSelected(
RapidOrderExampleSelected event,
Emitter<RapidOrderState> emit,
) {
if (state is RapidOrderInitial) {
final String cleanedExample = event.example.replaceAll('"', '');
emit((state as RapidOrderInitial).copyWith(message: cleanedExample));
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
abstract class RapidOrderEvent extends Equatable {
const RapidOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class RapidOrderMessageChanged extends RapidOrderEvent {
const RapidOrderMessageChanged(this.message);
final String message;
@override
List<Object?> get props => <Object?>[message];
}
class RapidOrderVoiceToggled extends RapidOrderEvent {
const RapidOrderVoiceToggled();
}
class RapidOrderSubmitted extends RapidOrderEvent {
const RapidOrderSubmitted();
}
class RapidOrderExampleSelected extends RapidOrderEvent {
const RapidOrderExampleSelected(this.example);
final String example;
@override
List<Object?> get props => <Object?>[example];
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
abstract class RapidOrderState extends Equatable {
const RapidOrderState();
@override
List<Object?> get props => <Object?>[];
}
class RapidOrderInitial extends RapidOrderState {
const RapidOrderInitial({
this.message = '',
this.isListening = false,
required this.examples,
});
final String message;
final bool isListening;
final List<String> examples;
@override
List<Object?> get props => <Object?>[message, isListening, examples];
RapidOrderInitial copyWith({
String? message,
bool? isListening,
List<String>? examples,
}) {
return RapidOrderInitial(
message: message ?? this.message,
isListening: isListening ?? this.isListening,
examples: examples ?? this.examples,
);
}
}
class RapidOrderSubmitting extends RapidOrderState {
const RapidOrderSubmitting();
}
class RapidOrderSuccess extends RapidOrderState {
const RapidOrderSuccess();
}
class RapidOrderFailure extends RapidOrderState {
const RapidOrderFailure(this.error);
final String error;
@override
List<Object?> get props => <Object?>[error];
}

View File

@@ -0,0 +1,3 @@
export 'recurring_order_bloc.dart';
export 'recurring_order_event.dart';
export 'recurring_order_state.dart';

View File

@@ -0,0 +1,354 @@
import 'package:client_create_order/src/domain/usecases/create_recurring_order_usecase.dart';
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 '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>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
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;
}
final int newDayIndex = event.date.weekday % 7;
final int? autoIndex = state.autoSelectedDayIndex;
List<String> days = List<String>.from(state.recurringDays);
if (autoIndex != null) {
final String oldDay = _dayLabels[autoIndex];
days.remove(oldDay);
final String newDay = _dayLabels[newDayIndex];
if (!days.contains(newDay)) {
days.add(newDay);
}
days = _sortDays(days);
}
emit(
state.copyWith(
startDate: event.date,
endDate: endDate,
recurringDays: days,
autoSelectedDayIndex: autoIndex == null ? null : newDayIndex,
),
);
}
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<String> days = List<String>.from(state.recurringDays);
final String label = _dayLabels[event.dayIndex];
int? autoIndex = state.autoSelectedDayIndex;
if (days.contains(label)) {
days.remove(label);
if (autoIndex == event.dayIndex) {
autoIndex = null;
}
} else {
days.add(label);
}
emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
}
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.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,
roleRates: roleRates,
);
await _createRecurringOrderUseCase(order);
emit(state.copyWith(status: RecurringOrderStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: RecurringOrderStatus.failure,
errorMessage: errorKey,
),
);
}
static List<String> _sortDays(List<String> days) {
days.sort(
(String a, String b) =>
_dayLabels.indexOf(a).compareTo(_dayLabels.indexOf(b)),
);
return days;
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,229 @@
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,
required this.autoSelectedDayIndex,
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);
final List<String> dayLabels = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
final int weekdayIndex = now.weekday % 7;
return RecurringOrderState(
startDate: start,
endDate: start.add(const Duration(days: 7)),
recurringDays: <String>[dayLabels[weekdayIndex]],
location: '',
eventName: '',
positions: const <RecurringOrderPosition>[
RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
autoSelectedDayIndex: weekdayIndex,
vendors: const <Vendor>[],
hubs: const <RecurringOrderHubOption>[],
roles: const <RecurringOrderRoleOption>[],
);
}
final DateTime startDate;
final DateTime endDate;
final List<String> recurringDays;
final String location;
final String eventName;
final List<RecurringOrderPosition> positions;
final int? autoSelectedDayIndex;
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<String>? recurringDays,
String? location,
String? eventName,
List<RecurringOrderPosition>? positions,
int? autoSelectedDayIndex,
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,
autoSelectedDayIndex: autoSelectedDayIndex ?? this.autoSelectedDayIndex,
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,
autoSelectedDayIndex,
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];
}

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import '../widgets/create_order/create_order_view.dart';
/// Main entry page for the client create order flow.
///
/// This page displays the [CreateOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its UI to other components.
class ClientCreateOrderPage extends StatelessWidget {
/// Creates a [ClientCreateOrderPage].
const ClientCreateOrderPage({super.key});
@override
Widget build(BuildContext context) {
return const CreateOrderView();
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/one_time_order/one_time_order_bloc.dart';
import '../widgets/one_time_order/one_time_order_view.dart';
/// Page for creating a one-time staffing order.
/// Users can specify the date, location, and multiple staff positions required.
///
/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
class OneTimeOrderPage extends StatelessWidget {
/// Creates a [OneTimeOrderPage].
const OneTimeOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<OneTimeOrderBloc>(
create: (BuildContext context) => Modular.get<OneTimeOrderBloc>(),
child: const OneTimeOrderView(),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/permanent_order/permanent_order_bloc.dart';
import '../widgets/permanent_order/permanent_order_view.dart';
/// Page for creating a permanent staffing order.
class PermanentOrderPage extends StatelessWidget {
/// Creates a [PermanentOrderPage].
const PermanentOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<PermanentOrderBloc>(
create: (BuildContext context) => Modular.get<PermanentOrderBloc>(),
child: const PermanentOrderView(),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/rapid_order/rapid_order_bloc.dart';
import '../widgets/rapid_order/rapid_order_view.dart';
/// Rapid Order Flow Page - Emergency staffing requests.
/// Features voice recognition simulation and quick example selection.
///
/// This page initializes the [RapidOrderBloc] and displays the [RapidOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
class RapidOrderPage extends StatelessWidget {
/// Creates a [RapidOrderPage].
const RapidOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<RapidOrderBloc>(
create: (BuildContext context) => Modular.get<RapidOrderBloc>(),
child: const RapidOrderView(),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/recurring_order/recurring_order_bloc.dart';
import '../widgets/recurring_order/recurring_order_view.dart';
/// Page for creating a recurring staffing order.
class RecurringOrderPage extends StatelessWidget {
/// Creates a [RecurringOrderPage].
const RecurringOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<RecurringOrderBloc>(
create: (BuildContext context) => Modular.get<RecurringOrderBloc>(),
child: const RecurringOrderView(),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:krow_domain/krow_domain.dart' as domain;
/// Order type constants for the create order feature
const List<domain.OrderType> orderTypes = <domain.OrderType>[
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
// domain.OrderType(
// id: 'rapid',
// titleKey: 'client_create_order.types.rapid',
// descriptionKey: 'client_create_order.types.rapid_desc',
// ),
domain.OrderType(
id: 'one-time',
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
),
domain.OrderType(
id: 'recurring',
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
),
domain.OrderType(
id: 'permanent',
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
),
];

View File

@@ -0,0 +1,93 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/widgets.dart';
/// Metadata for styling order type cards based on their ID.
class OrderTypeUiMetadata {
/// Creates an [OrderTypeUiMetadata].
const OrderTypeUiMetadata({
required this.icon,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
/// Factory to get metadata based on order type ID.
factory OrderTypeUiMetadata.fromId({required String id}) {
switch (id) {
case 'rapid':
return OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: UiColors.iconError.withAlpha(24),
borderColor: UiColors.iconError,
iconBackgroundColor: UiColors.iconError.withAlpha(24),
iconColor: UiColors.iconError,
textColor: UiColors.iconError,
descriptionColor: UiColors.iconError,
);
case 'one-time':
return OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: UiColors.primary.withAlpha(24),
borderColor: UiColors.primary,
iconBackgroundColor: UiColors.primary.withAlpha(24),
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.primary,
);
case 'permanent':
return OrderTypeUiMetadata(
icon: UiIcons.users,
backgroundColor: UiColors.textSuccess.withAlpha(24),
borderColor: UiColors.textSuccess,
iconBackgroundColor: UiColors.textSuccess.withAlpha(24),
iconColor: UiColors.textSuccess,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
);
case 'recurring':
return OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
borderColor: const Color.fromARGB(255, 170, 10, 223),
iconBackgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
iconColor: const Color.fromARGB(255, 170, 10, 223),
textColor: const Color.fromARGB(255, 170, 10, 223),
descriptionColor: const Color.fromARGB(255, 170, 10, 223),
);
default:
return const OrderTypeUiMetadata(
icon: UiIcons.help,
backgroundColor: UiColors.bgSecondary,
borderColor: UiColors.border,
iconBackgroundColor: UiColors.iconSecondary,
iconColor: UiColors.white,
textColor: UiColors.textPrimary,
descriptionColor: UiColors.textSecondary,
);
}
}
/// Icon for the order type.
final IconData icon;
/// Background color for the card.
final Color backgroundColor;
/// Border color for the card.
final Color borderColor;
/// Background color for the icon.
final Color iconBackgroundColor;
/// Color for the icon.
final Color iconColor;
/// Color for the title text.
final Color textColor;
/// Color for the description text.
final Color descriptionColor;
}

View File

@@ -0,0 +1,112 @@
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:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../utils/constants/order_types.dart';
import '../../utils/ui_entities/order_type_ui_metadata.dart';
import '../order_type_card.dart';
/// Helper to map keys to localized strings.
String _getTranslation({required String key}) {
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
return t.client_create_order.types.rapid_desc;
} else if (key == 'client_create_order.types.one_time') {
return t.client_create_order.types.one_time;
} else if (key == 'client_create_order.types.one_time_desc') {
return t.client_create_order.types.one_time_desc;
} else if (key == 'client_create_order.types.recurring') {
return t.client_create_order.types.recurring;
} else if (key == 'client_create_order.types.recurring_desc') {
return t.client_create_order.types.recurring_desc;
} else if (key == 'client_create_order.types.permanent') {
return t.client_create_order.types.permanent;
} else if (key == 'client_create_order.types.permanent_desc') {
return t.client_create_order.types.permanent_desc;
}
return key;
}
/// The main content of the Create Order page.
class CreateOrderView extends StatelessWidget {
/// Creates a [CreateOrderView].
const CreateOrderView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: UiAppBar(
title: t.client_create_order.title,
onLeadingPressed: () => Modular.to.toClientHome(),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space6),
child: Text(
t.client_create_order.section_title,
style: UiTypography.body2m.textDescription,
),
),
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final OrderType type = orderTypes[index];
final OrderTypeUiMetadata ui = OrderTypeUiMetadata.fromId(
id: type.id,
);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(key: type.descriptionKey),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.toCreateOrderRapid();
break;
case 'one-time':
Modular.to.toCreateOrderOneTime();
break;
case 'recurring':
Modular.to.toCreateOrderRecurring();
break;
case 'permanent':
Modular.to.toCreateOrderPermanent();
break;
}
},
);
},
),
),
],
),
),
),
);
}
}

View File

@@ -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 one-time order form.
/// Matches the prototype input field style.
class OneTimeOrderDatePicker extends StatefulWidget {
/// Creates a [OneTimeOrderDatePicker].
const OneTimeOrderDatePicker({
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<OneTimeOrderDatePicker> createState() => _OneTimeOrderDatePickerState();
}
class _OneTimeOrderDatePickerState extends State<OneTimeOrderDatePicker> {
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(OneTimeOrderDatePicker 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);
}
},
);
}
}

View File

@@ -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 one-time order form.
class OneTimeOrderEventNameInput extends StatefulWidget {
const OneTimeOrderEventNameInput({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
final String label;
final String value;
final ValueChanged<String> onChanged;
@override
State<OneTimeOrderEventNameInput> createState() =>
_OneTimeOrderEventNameInputState();
}
class _OneTimeOrderEventNameInputState
extends State<OneTimeOrderEventNameInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void didUpdateWidget(OneTimeOrderEventNameInput 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,
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the one-time order flow with a colored background.
class OneTimeOrderHeader extends StatelessWidget {
/// Creates a [OneTimeOrderHeader].
const OneTimeOrderHeader({
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),
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A location input field for the one-time order form.
/// Matches the prototype input field style.
class OneTimeOrderLocationInput extends StatefulWidget {
/// Creates a [OneTimeOrderLocationInput].
const OneTimeOrderLocationInput({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
/// The label text to display above the field.
final String label;
/// The current location value.
final String value;
/// Callback when the location value changes.
final ValueChanged<String> onChanged;
@override
State<OneTimeOrderLocationInput> createState() =>
_OneTimeOrderLocationInputState();
}
class _OneTimeOrderLocationInputState extends State<OneTimeOrderLocationInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(OneTimeOrderLocationInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
}
@override
Widget build(BuildContext context) {
return UiTextField(
label: widget.label,
controller: _controller,
onChanged: widget.onChanged,
hintText: 'Enter address',
prefixIcon: UiIcons.mapPin,
);
}
}

View File

@@ -0,0 +1,349 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/one_time_order/one_time_order_state.dart';
/// A card widget for editing a specific position in a one-time order.
/// Matches the prototype layout while using design system tokens.
class OneTimeOrderPositionCard extends StatelessWidget {
/// Creates a [OneTimeOrderPositionCard].
const OneTimeOrderPositionCard({
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 OneTimeOrderPosition 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<OneTimeOrderPosition> 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<OneTimeOrderRoleOption> 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(
(OneTimeOrderRoleOption role) => DropdownMenuItem<String>(
value: role.id,
child: Text(
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
style: UiTypography.body2r.textPrimary,
),
),
)
.toList();
final bool hasSelected = roles.any((OneTimeOrderRoleOption 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;
}
}

View File

@@ -0,0 +1,52 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for sections in the one-time order form.
class OneTimeOrderSectionHeader extends StatelessWidget {
/// Creates a [OneTimeOrderSectionHeader].
const OneTimeOrderSectionHeader({
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,
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A view to display when a one-time order has been successfully created.
/// Matches the prototype success view layout with a gradient background and centered card.
class OneTimeOrderSuccessView extends StatelessWidget {
/// Creates a [OneTimeOrderSuccessView].
const OneTimeOrderSuccessView({
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,
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,328 @@
import 'package:core_localization/core_localization.dart';
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 'package:krow_domain/krow_domain.dart';
import '../../blocs/one_time_order/one_time_order_bloc.dart';
import '../../blocs/one_time_order/one_time_order_event.dart';
import '../../blocs/one_time_order/one_time_order_state.dart';
import 'one_time_order_date_picker.dart';
import 'one_time_order_event_name_input.dart';
import 'one_time_order_header.dart';
import 'one_time_order_position_card.dart';
import 'one_time_order_section_header.dart';
import 'one_time_order_success_view.dart';
/// The main content of the One-Time Order page.
class OneTimeOrderView extends StatelessWidget {
/// Creates a [OneTimeOrderView].
const OneTimeOrderView({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return BlocConsumer<OneTimeOrderBloc, OneTimeOrderState>(
listener: (BuildContext context, OneTimeOrderState state) {
if (state.status == OneTimeOrderStatus.failure &&
state.errorMessage != null) {
UiSnackbar.show(
context,
message: translateErrorKey(state.errorMessage!),
type: UiSnackbarType.error,
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
);
}
},
builder: (BuildContext context, OneTimeOrderState state) {
if (state.status == OneTimeOrderStatus.success) {
return OneTimeOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pushNamedAndRemoveUntil(
ClientPaths.orders,
(_) => false,
arguments: <String, dynamic>{
'initialDate': state.date.toIso8601String(),
},
),
);
}
if (state.vendors.isEmpty &&
state.status != OneTimeOrderStatus.loading) {
return Scaffold(
body: Column(
children: <Widget>[
OneTimeOrderHeader(
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>[
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
),
Expanded(
child: Stack(
children: <Widget>[
_OneTimeOrderForm(state: state),
if (state.status == OneTimeOrderStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: state.status == OneTimeOrderStatus.loading
? labels.creating
: labels.create_order,
isLoading: state.status == OneTimeOrderStatus.loading,
onPressed: state.isValid
? () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderSubmitted())
: null,
),
],
),
);
},
);
}
}
class _OneTimeOrderForm extends StatelessWidget {
const _OneTimeOrderForm({required this.state});
final OneTimeOrderState state;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.create_your_order,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderEventNameInput(
label: 'ORDER NAME',
value: state.eventName,
onChanged: (String value) => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderEventNameChanged(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<OneTimeOrderBloc>(
context,
).add(OneTimeOrderVendorChanged(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),
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderDateChanged(date)),
),
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<OneTimeOrderHubOption>(
isExpanded: true,
value: state.selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OneTimeOrderHubOption? hub) {
if (hub != null) {
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderHubChanged(hub));
}
},
items: state.hubs.map((OneTimeOrderHubOption hub) {
return DropdownMenuItem<OneTimeOrderHubOption>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space3),
// Positions List
...state.positions.asMap().entries.map((
MapEntry<int, OneTimeOrderPosition> entry,
) {
final int index = entry.key;
final OneTimeOrderPosition position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: OneTimeOrderPositionCard(
index: index,
position: position,
isRemovable: state.positions.length > 1,
positionLabel: labels.positions_title,
roleLabel: labels.select_role,
workersLabel: labels.workers_label,
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
roles: state.roles,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionUpdated(index, updated));
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionRemoved(index));
},
),
);
}),
],
);
}
}
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,
),
),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A card widget representing an order type in the creation flow.
class OrderTypeCard extends StatelessWidget {
/// Creates an [OrderTypeCard].
const OrderTypeCard({
required this.icon,
required this.title,
required this.description,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
required this.onTap,
super.key,
});
/// Icon to display at the top of the card.
final IconData icon;
/// Main title of the order type.
final String title;
/// Brief description of what this order type entails.
final String description;
/// Background color of the card.
final Color backgroundColor;
/// Color of the card's border.
final Color borderColor;
/// Background color for the icon container.
final Color iconBackgroundColor;
/// Color of the icon itself.
final Color iconColor;
/// Color of the title text.
final Color textColor;
/// Color of the description text.
final Color descriptionColor;
/// Callback when the card is tapped.
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: borderColor, width: 0.75),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
width: 48,
height: 48,
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: iconBackgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(title, style: UiTypography.body1b.copyWith(color: textColor)),
Expanded(
child: Text(
description,
style: UiTypography.footnote1r.copyWith(
color: descriptionColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}

View File

@@ -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 permanent order form.
class PermanentOrderDatePicker extends StatefulWidget {
/// Creates a [PermanentOrderDatePicker].
const PermanentOrderDatePicker({
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<PermanentOrderDatePicker> createState() =>
_PermanentOrderDatePickerState();
}
class _PermanentOrderDatePickerState extends State<PermanentOrderDatePicker> {
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(PermanentOrderDatePicker 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);
}
},
);
}
}

View File

@@ -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 permanent order form.
class PermanentOrderEventNameInput extends StatefulWidget {
const PermanentOrderEventNameInput({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
final String label;
final String value;
final ValueChanged<String> onChanged;
@override
State<PermanentOrderEventNameInput> createState() =>
_PermanentOrderEventNameInputState();
}
class _PermanentOrderEventNameInputState
extends State<PermanentOrderEventNameInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void didUpdateWidget(PermanentOrderEventNameInput 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,
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the permanent order flow with a colored background.
class PermanentOrderHeader extends StatelessWidget {
/// Creates a [PermanentOrderHeader].
const PermanentOrderHeader({
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),
),
),
],
),
],
),
);
}
}

View File

@@ -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/permanent_order/permanent_order_state.dart';
/// A card widget for editing a specific position in a permanent order.
class PermanentOrderPositionCard extends StatelessWidget {
/// Creates a [PermanentOrderPositionCard].
const PermanentOrderPositionCard({
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 PermanentOrderPosition 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<PermanentOrderPosition> 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<PermanentOrderRoleOption> 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(
(PermanentOrderRoleOption role) => DropdownMenuItem<String>(
value: role.id,
child: Text(
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
style: UiTypography.body2r.textPrimary,
),
),
)
.toList();
final bool hasSelected = roles.any((PermanentOrderRoleOption 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;
}
}

View File

@@ -0,0 +1,52 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for sections in the permanent order form.
class PermanentOrderSectionHeader extends StatelessWidget {
/// Creates a [PermanentOrderSectionHeader].
const PermanentOrderSectionHeader({
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,
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,104 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A view to display when a permanent order has been successfully created.
class PermanentOrderSuccessView extends StatelessWidget {
/// Creates a [PermanentOrderSuccessView].
const PermanentOrderSuccessView({
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,
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,440 @@
import 'package:core_localization/core_localization.dart';
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 'package:krow_domain/krow_domain.dart' show Vendor;
import '../../blocs/permanent_order/permanent_order_bloc.dart';
import '../../blocs/permanent_order/permanent_order_event.dart';
import '../../blocs/permanent_order/permanent_order_state.dart';
import 'permanent_order_date_picker.dart';
import 'permanent_order_event_name_input.dart';
import 'permanent_order_header.dart';
import 'permanent_order_position_card.dart';
import 'permanent_order_section_header.dart';
import 'permanent_order_success_view.dart';
/// The main content of the Permanent Order page.
class PermanentOrderView extends StatelessWidget {
/// Creates a [PermanentOrderView].
const PermanentOrderView({super.key});
DateTime _firstPermanentShiftDate(
DateTime startDate,
List<String> permanentDays,
) {
final DateTime start = DateTime(startDate.year, startDate.month, startDate.day);
final DateTime end = start.add(const Duration(days: 29));
final Set<String> selected = permanentDays.toSet();
for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) {
if (selected.contains(_weekdayLabel(day))) {
return day;
}
}
return start;
}
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:
return 'SUN';
default:
return 'SUN';
}
}
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return BlocConsumer<PermanentOrderBloc, PermanentOrderState>(
listener: (BuildContext context, PermanentOrderState state) {
if (state.status == PermanentOrderStatus.failure &&
state.errorMessage != null) {
final String message = translateErrorKey(state.errorMessage!);
UiSnackbar.show(
context,
message: message,
type: UiSnackbarType.error,
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
);
}
},
builder: (BuildContext context, PermanentOrderState state) {
if (state.status == PermanentOrderStatus.success) {
final DateTime initialDate = _firstPermanentShiftDate(
state.startDate,
state.permanentDays,
);
return PermanentOrderSuccessView(
title: labels.title,
message: labels.subtitle,
buttonLabel: oneTimeLabels.back_to_orders,
onDone: () => Modular.to.pushNamedAndRemoveUntil(
ClientPaths.orders,
(_) => false,
arguments: <String, dynamic>{
'initialDate': initialDate.toIso8601String(),
},
),
);
}
if (state.vendors.isEmpty &&
state.status != PermanentOrderStatus.loading) {
return Scaffold(
body: Column(
children: <Widget>[
PermanentOrderHeader(
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>[
PermanentOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
),
Expanded(
child: Stack(
children: <Widget>[
_PermanentOrderForm(state: state),
if (state.status == PermanentOrderStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: state.status == PermanentOrderStatus.loading
? oneTimeLabels.creating
: oneTimeLabels.create_order,
isLoading: state.status == PermanentOrderStatus.loading,
onPressed: state.isValid
? () => BlocProvider.of<PermanentOrderBloc>(
context,
).add(const PermanentOrderSubmitted())
: null,
),
],
),
);
},
);
}
}
class _PermanentOrderForm extends StatelessWidget {
const _PermanentOrderForm({required this.state});
final PermanentOrderState state;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
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),
PermanentOrderEventNameInput(
label: 'ORDER NAME',
value: state.eventName,
onChanged: (String value) => BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderEventNameChanged(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<PermanentOrderBloc>(
context,
).add(PermanentOrderVendorChanged(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),
PermanentOrderDatePicker(
label: 'Start Date',
value: state.startDate,
onChanged: (DateTime date) => BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderStartDateChanged(date)),
),
const SizedBox(height: UiConstants.space4),
Text('Permanent Days', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
_PermanentDaysSelector(
selectedDays: state.permanentDays,
onToggle: (int dayIndex) => BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderDayToggled(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<PermanentOrderHubOption>(
isExpanded: true,
value: state.selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (PermanentOrderHubOption? hub) {
if (hub != null) {
BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderHubChanged(hub));
}
},
items: state.hubs.map((PermanentOrderHubOption hub) {
return DropdownMenuItem<PermanentOrderHubOption>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space6),
PermanentOrderSectionHeader(
title: oneTimeLabels.positions_title,
actionLabel: oneTimeLabels.add_position,
onAction: () => BlocProvider.of<PermanentOrderBloc>(
context,
).add(const PermanentOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space3),
// Positions List
...state.positions.asMap().entries.map((
MapEntry<int, PermanentOrderPosition> entry,
) {
final int index = entry.key;
final PermanentOrderPosition position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: PermanentOrderPositionCard(
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: (PermanentOrderPosition updated) {
BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderPositionUpdated(index, updated));
},
onRemoved: () {
BlocProvider.of<PermanentOrderBloc>(
context,
).add(PermanentOrderPositionRemoved(index));
},
),
);
}),
],
);
}
}
class _PermanentDaysSelector extends StatelessWidget {
const _PermanentDaysSelector({
required this.selectedDays,
required this.onToggle,
});
final List<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[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(
labelsShort[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,
),
),
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A card displaying an example message for a rapid order.
class RapidOrderExampleCard extends StatelessWidget {
/// Creates a [RapidOrderExampleCard].
const RapidOrderExampleCard({
required this.example,
required this.isHighlighted,
required this.label,
required this.onTap,
super.key,
});
/// The example text.
final String example;
/// Whether this is the first (highlighted) example.
final bool isHighlighted;
/// The label for the example prefix (e.g., "Example:").
final String label;
/// Callback when the card is tapped.
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: isHighlighted
? UiColors.accent.withValues(alpha: 0.15)
: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: isHighlighted ? UiColors.accent : UiColors.border,
),
),
child: RichText(
text: TextSpan(
style: UiTypography.body2r.textPrimary,
children: <InlineSpan>[
TextSpan(text: label, style: UiTypography.body2b.textPrimary),
TextSpan(text: ' $example'),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the rapid order flow with a gradient background.
class RapidOrderHeader extends StatelessWidget {
/// Creates a [RapidOrderHeader].
const RapidOrderHeader({
required this.title,
required this.subtitle,
required this.date,
required this.time,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// The formatted current date.
final String date;
/// The formatted current time.
final String time;
/// 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,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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>[
Row(
children: <Widget>[
const Icon(UiIcons.zap, color: UiColors.accent, size: 18),
const SizedBox(width: UiConstants.space2),
Text(
title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
),
],
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
date,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
Text(
time,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A view to display when a rapid order has been successfully created.
class RapidOrderSuccessView extends StatelessWidget {
/// Creates a [RapidOrderSuccessView].
const RapidOrderSuccessView({
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: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
UiColors.primary,
UiColors.primary.withValues(alpha: 0.85),
],
),
),
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,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: UiColors.accent,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
UiIcons.zap,
color: UiColors.textPrimary,
size: 32,
),
),
),
const SizedBox(height: UiConstants.space6),
Text(title, style: UiTypography.headline1m.textPrimary),
const SizedBox(height: UiConstants.space3),
Text(
message,
textAlign: TextAlign.center,
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: UiConstants.space8),
SizedBox(
width: double.infinity,
child: UiButton.primary(
text: buttonLabel,
onPressed: onDone,
size: UiButtonSize.large,
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,308 @@
import 'package:core_localization/core_localization.dart';
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:krow_core/core.dart';
import '../../blocs/rapid_order/rapid_order_bloc.dart';
import '../../blocs/rapid_order/rapid_order_event.dart';
import '../../blocs/rapid_order/rapid_order_state.dart';
import 'rapid_order_example_card.dart';
import 'rapid_order_header.dart';
import 'rapid_order_success_view.dart';
/// The main content of the Rapid Order page.
class RapidOrderView extends StatelessWidget {
/// Creates a [RapidOrderView].
const RapidOrderView({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
return BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderSuccess) {
return RapidOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.navigate(ClientPaths.orders),
);
}
return const _RapidOrderForm();
},
);
}
}
class _RapidOrderForm extends StatefulWidget {
const _RapidOrderForm();
@override
State<_RapidOrderForm> createState() => _RapidOrderFormState();
}
class _RapidOrderFormState extends State<_RapidOrderForm> {
final TextEditingController _messageController = TextEditingController();
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
final DateTime now = DateTime.now();
final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now);
final String timeStr = DateFormat('h:mm a').format(now);
return BlocListener<RapidOrderBloc, RapidOrderState>(
listener: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderInitial) {
if (_messageController.text != state.message) {
_messageController.text = state.message;
_messageController.selection = TextSelection.fromPosition(
TextPosition(offset: _messageController.text.length),
);
}
} else if (state is RapidOrderFailure) {
UiSnackbar.show(
context,
message: translateErrorKey(state.error),
type: UiSnackbarType.error,
);
}
},
child: Scaffold(
body: Column(
children: <Widget>[
RapidOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
date: dateStr,
time: timeStr,
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
labels.tell_us,
style: UiTypography.headline3m.textPrimary,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: UiColors.destructive,
borderRadius: UiConstants.radiusSm,
),
child: Text(
labels.urgent_badge,
style: UiTypography.footnote2b.copyWith(
color: UiColors.white,
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Main Card
Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
final RapidOrderInitial? initialState =
state is RapidOrderInitial ? state : null;
final bool isSubmitting =
state is RapidOrderSubmitting;
return Column(
children: <Widget>[
// Icon
const _AnimatedZapIcon(),
const SizedBox(height: UiConstants.space4),
Text(
labels.need_staff,
style: UiTypography.headline2m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
labels.type_or_speak,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space6),
// Examples
if (initialState != null)
...initialState.examples.asMap().entries.map((
MapEntry<int, String> entry,
) {
final int index = entry.key;
final String example = entry.value;
final bool isHighlighted = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2,
),
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context,
).add(
RapidOrderExampleSelected(example),
),
),
);
}),
const SizedBox(height: UiConstants.space4),
// Input
UiTextField(
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(
context,
).add(RapidOrderMessageChanged(value));
},
hintText: labels.hint,
),
const SizedBox(height: UiConstants.space4),
// Actions
_RapidOrderActions(
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty:
initialState != null &&
initialState.message.trim().isEmpty,
),
],
);
},
),
),
],
),
),
),
],
),
),
);
}
}
class _AnimatedZapIcon extends StatelessWidget {
const _AnimatedZapIcon();
@override
Widget build(BuildContext context) {
return Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.destructive.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(UiIcons.zap, color: UiColors.white, size: 32),
);
}
}
class _RapidOrderActions extends StatelessWidget {
const _RapidOrderActions({
required this.labels,
required this.isSubmitting,
required this.isListening,
required this.isMessageEmpty,
});
final TranslationsClientCreateOrderRapidEn labels;
final bool isSubmitting;
final bool isListening;
final bool isMessageEmpty;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderVoiceToggled()),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
: null,
side: isListening
? const BorderSide(color: UiColors.destructive)
: null,
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
text: isSubmitting ? labels.sending : labels.send,
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () {
print('RapidOrder send pressed');
BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderSubmitted());
},
),
),
],
);
}
}

View File

@@ -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);
}
},
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
),
),
],
),
],
),
);
}
}

View File

@@ -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/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;
}
}

View File

@@ -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,
),
],
),
),
],
);
}
}

View File

@@ -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,
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,457 @@
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/recurring_order_bloc.dart';
import '../../blocs/recurring_order/recurring_order_event.dart';
import '../../blocs/recurring_order/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});
DateTime _firstRecurringShiftDate(
DateTime startDate,
DateTime endDate,
List<String> recurringDays,
) {
final DateTime start = DateTime(startDate.year, startDate.month, startDate.day);
final DateTime end = DateTime(endDate.year, endDate.month, endDate.day);
final Set<String> selected = recurringDays.toSet();
for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) {
if (selected.contains(_weekdayLabel(day))) {
return day;
}
}
return start;
}
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:
return 'SUN';
default:
return 'SUN';
}
}
@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) {
final DateTime maxEndDate =
state.startDate.add(const Duration(days: 29));
final DateTime effectiveEndDate =
state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate;
final DateTime initialDate = _firstRecurringShiftDate(
state.startDate,
effectiveEndDate,
state.recurringDays,
);
return RecurringOrderSuccessView(
title: labels.title,
message: labels.subtitle,
buttonLabel: oneTimeLabels.back_to_orders,
onDone: () => Modular.to.pushNamedAndRemoveUntil(
ClientPaths.orders,
(_) => false,
arguments: <String, dynamic>{
'initialDate': initialDate.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<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[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(
labelsShort[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,
),
),
);
}
}

View File

@@ -0,0 +1,33 @@
name: client_create_order
description: Client create order feature
version: 0.0.1
publish_to: none
resolution: workspace
environment:
sdk: ">=3.10.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
flutter_modular: ^6.3.2
equatable: ^2.0.5
intl: 0.20.2
design_system:
path: ../../../../design_system
core_localization:
path: ../../../../core_localization
krow_domain:
path: ../../../../domain
krow_core:
path: ../../../../core
krow_data_connect:
path: ../../../../data_connect
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.5

View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: android
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: ios
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: linux
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: macos
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: web
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
- platform: windows
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,3 @@
# orders
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.orders"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.orders"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="orders"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.orders
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.orders;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

Some files were not shown because too many files have changed in this diff Show More