refactor: remove recurring order widgets and related functionality
- Deleted RecurringOrderDatePicker, RecurringOrderEventNameInput, RecurringOrderHeader, RecurringOrderPositionCard, RecurringOrderSectionHeader, RecurringOrderSuccessView, and RecurringOrderView. - Removed associated imports and references in the codebase. - Updated pubspec.yaml to include client_orders_common dependency. - Cleaned up the RapidOrderActions widget by removing debug print statement.
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.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 '../widgets/one_time_order/one_time_order_view.dart';
|
||||
import '../blocs/one_time_order/one_time_order_event.dart';
|
||||
import '../blocs/one_time_order/one_time_order_state.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.
|
||||
/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView]
|
||||
/// from the common orders package. It follows the Krow Clean Architecture by being
|
||||
/// a [StatelessWidget] and mapping local BLoC state to generic UI models.
|
||||
class OneTimeOrderPage extends StatelessWidget {
|
||||
/// Creates a [OneTimeOrderPage].
|
||||
const OneTimeOrderPage({super.key});
|
||||
@@ -18,7 +22,101 @@ class OneTimeOrderPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OneTimeOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<OneTimeOrderBloc>(),
|
||||
child: const OneTimeOrderView(),
|
||||
child: BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
|
||||
builder: (BuildContext context, OneTimeOrderState state) {
|
||||
final OneTimeOrderBloc bloc = BlocProvider.of<OneTimeOrderBloc>(context);
|
||||
|
||||
return OneTimeOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
date: state.date,
|
||||
selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) => bloc.add(OneTimeOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) => bloc.add(OneTimeOrderVendorChanged(val)),
|
||||
onDateChanged: (DateTime val) => bloc.add(OneTimeOrderDateChanged(val)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final OneTimeOrderHubOption originalHub = state.hubs.firstWhere((OneTimeOrderHubOption h) => h.id == val.id);
|
||||
bloc.add(OneTimeOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () => bloc.add(const OneTimeOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final OneTimeOrderPosition original = state.positions[index];
|
||||
final OneTimeOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(OneTimeOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) => bloc.add(OneTimeOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const OneTimeOrderSubmitted()),
|
||||
onDone: () => Modular.to.pushNamedAndRemoveUntil(
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': state.date.toIso8601String(),
|
||||
},
|
||||
),
|
||||
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(OneTimeOrderStatus status) {
|
||||
switch (status) {
|
||||
case OneTimeOrderStatus.initial:
|
||||
return OrderFormStatus.initial;
|
||||
case OneTimeOrderStatus.loading:
|
||||
return OrderFormStatus.loading;
|
||||
case OneTimeOrderStatus.success:
|
||||
return OrderFormStatus.success;
|
||||
case OneTimeOrderStatus.failure:
|
||||
return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(OneTimeOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(OneTimeOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(OneTimeOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide PermanentOrderPosition;
|
||||
import '../blocs/permanent_order/permanent_order_bloc.dart';
|
||||
import '../widgets/permanent_order/permanent_order_view.dart';
|
||||
import '../blocs/permanent_order/permanent_order_event.dart';
|
||||
import '../blocs/permanent_order/permanent_order_state.dart';
|
||||
|
||||
/// Page for creating a permanent staffing order.
|
||||
class PermanentOrderPage extends StatelessWidget {
|
||||
@@ -13,7 +17,133 @@ class PermanentOrderPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<PermanentOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<PermanentOrderBloc>(),
|
||||
child: const PermanentOrderView(),
|
||||
child: BlocBuilder<PermanentOrderBloc, PermanentOrderState>(
|
||||
builder: (BuildContext context, PermanentOrderState state) {
|
||||
final PermanentOrderBloc bloc = BlocProvider.of<PermanentOrderBloc>(context);
|
||||
|
||||
return PermanentOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
startDate: state.startDate,
|
||||
permanentDays: state.permanentDays,
|
||||
selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) => bloc.add(PermanentOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) => bloc.add(PermanentOrderVendorChanged(val)),
|
||||
onStartDateChanged: (DateTime val) => bloc.add(PermanentOrderStartDateChanged(val)),
|
||||
onDayToggled: (int index) => bloc.add(PermanentOrderDayToggled(index)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final PermanentOrderHubOption originalHub = state.hubs.firstWhere((PermanentOrderHubOption h) => h.id == val.id);
|
||||
bloc.add(PermanentOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () => bloc.add(const PermanentOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final PermanentOrderPosition original = state.positions[index];
|
||||
final PermanentOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(PermanentOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) => bloc.add(PermanentOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const PermanentOrderSubmitted()),
|
||||
onDone: () {
|
||||
final DateTime initialDate = _firstPermanentShiftDate(
|
||||
state.startDate,
|
||||
state.permanentDays,
|
||||
);
|
||||
Modular.to.pushNamedAndRemoveUntil(
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': initialDate.toIso8601String(),
|
||||
},
|
||||
);
|
||||
},
|
||||
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(PermanentOrderStatus status) {
|
||||
switch (status) {
|
||||
case PermanentOrderStatus.initial: return OrderFormStatus.initial;
|
||||
case PermanentOrderStatus.loading: return OrderFormStatus.loading;
|
||||
case PermanentOrderStatus.success: return OrderFormStatus.success;
|
||||
case PermanentOrderStatus.failure: return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(PermanentOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(PermanentOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(PermanentOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak ?? 'NO_BREAK',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide RecurringOrderPosition;
|
||||
import '../blocs/recurring_order/recurring_order_bloc.dart';
|
||||
import '../widgets/recurring_order/recurring_order_view.dart';
|
||||
import '../blocs/recurring_order/recurring_order_event.dart';
|
||||
import '../blocs/recurring_order/recurring_order_state.dart';
|
||||
|
||||
/// Page for creating a recurring staffing order.
|
||||
class RecurringOrderPage extends StatelessWidget {
|
||||
@@ -13,7 +17,141 @@ class RecurringOrderPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RecurringOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<RecurringOrderBloc>(),
|
||||
child: const RecurringOrderView(),
|
||||
child: BlocBuilder<RecurringOrderBloc, RecurringOrderState>(
|
||||
builder: (BuildContext context, RecurringOrderState state) {
|
||||
final RecurringOrderBloc bloc = BlocProvider.of<RecurringOrderBloc>(context);
|
||||
|
||||
return RecurringOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
recurringDays: state.recurringDays,
|
||||
selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) => bloc.add(RecurringOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) => bloc.add(RecurringOrderVendorChanged(val)),
|
||||
onStartDateChanged: (DateTime val) => bloc.add(RecurringOrderStartDateChanged(val)),
|
||||
onEndDateChanged: (DateTime val) => bloc.add(RecurringOrderEndDateChanged(val)),
|
||||
onDayToggled: (int index) => bloc.add(RecurringOrderDayToggled(index)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final RecurringOrderHubOption originalHub = state.hubs.firstWhere((RecurringOrderHubOption h) => h.id == val.id);
|
||||
bloc.add(RecurringOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () => bloc.add(const RecurringOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final RecurringOrderPosition original = state.positions[index];
|
||||
final RecurringOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(RecurringOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) => bloc.add(RecurringOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const RecurringOrderSubmitted()),
|
||||
onDone: () {
|
||||
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,
|
||||
);
|
||||
|
||||
Modular.to.pushNamedAndRemoveUntil(
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': initialDate.toIso8601String(),
|
||||
},
|
||||
);
|
||||
},
|
||||
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(RecurringOrderStatus status) {
|
||||
switch (status) {
|
||||
case RecurringOrderStatus.initial: return OrderFormStatus.initial;
|
||||
case RecurringOrderStatus.loading: return OrderFormStatus.loading;
|
||||
case RecurringOrderStatus.success: return OrderFormStatus.success;
|
||||
case RecurringOrderStatus.failure: return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(RecurringOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(RecurringOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(RecurringOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak ?? 'NO_BREAK',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -295,7 +295,6 @@ class _RapidOrderActions extends StatelessWidget {
|
||||
onPressed: isSubmitting || isMessageEmpty
|
||||
? null
|
||||
: () {
|
||||
print('RapidOrder send pressed');
|
||||
BlocProvider.of<RapidOrderBloc>(
|
||||
context,
|
||||
).add(const RapidOrderSubmitted());
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ dependencies:
|
||||
path: ../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../../data_connect
|
||||
client_orders_common:
|
||||
path: ../orders_common
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
|
||||
Reference in New Issue
Block a user