Refactor create order UI and add widget components

Refactored the client create order flow to use new modular widget components for one-time and rapid order forms, improving code organization and reusability. Updated UI colors, app bar usage, and extracted logic for date picker, location input, position card, section header, and success views. Added placeholder pages for recurring and permanent order types. Updated Spanish localization for new order types.
This commit is contained in:
Achintha Isuru
2026-01-22 17:03:17 -05:00
parent 4b3125de1a
commit 96ff173855
15 changed files with 1174 additions and 1013 deletions

View File

@@ -287,6 +287,16 @@
"creating": "Creando...",
"success_title": "¡Orden Creada!",
"success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto."
},
"recurring": {
"title": "Orden Recurrente",
"subtitle": "Cobertura continua semanal/mensual",
"placeholder": "Flujo de Orden Recurrente (Trabajo en Progreso)"
},
"permanent": {
"title": "Orden Permanente",
"subtitle": "Colocación de personal a largo plazo",
"placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)"
}
}
}

View File

@@ -8,9 +8,10 @@ import '../blocs/client_create_order_bloc.dart';
import '../blocs/client_create_order_event.dart';
import '../blocs/client_create_order_state.dart';
import '../navigation/client_create_order_navigator.dart';
import '../widgets/order_type_card.dart';
/// One-time helper to map keys to translations since they are dynamic in BLoC state
String _getTranslation(String key) {
/// Helper to map keys to localized strings.
String _getTranslation({required String key}) {
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
@@ -31,7 +32,10 @@ String _getTranslation(String key) {
return key;
}
/// Main entry page for the client create order flow.
/// Allows the user to select the type of order they want to create.
class ClientCreateOrderPage extends StatelessWidget {
/// Creates a [ClientCreateOrderPage].
const ClientCreateOrderPage({super.key});
@override
@@ -50,22 +54,10 @@ class _CreateOrderView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
title: Text(
t.client_create_order.title,
style: UiTypography.headline3m.textPrimary,
),
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: t.client_create_order.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: SafeArea(
child: Padding(
@@ -104,13 +96,13 @@ class _CreateOrderView extends StatelessWidget {
itemBuilder: (BuildContext context, int index) {
final OrderType type = state.orderTypes[index];
final _OrderTypeUiMetadata ui =
_OrderTypeUiMetadata.fromId(type.id);
_OrderTypeUiMetadata.fromId(id: type.id);
return _OrderTypeCard(
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(type.titleKey),
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
type.descriptionKey,
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
@@ -150,74 +142,8 @@ class _CreateOrderView extends StatelessWidget {
}
}
class _OrderTypeCard extends StatelessWidget {
const _OrderTypeCard({
required this.icon,
required this.title,
required this.description,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
required this.onTap,
});
final IconData icon;
final String title;
final String description;
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: borderColor, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
width: 48,
height: 48,
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: iconBackgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(
title,
style: UiTypography.body2b.copyWith(color: textColor),
),
const SizedBox(height: UiConstants.space1),
Text(
description,
style: UiTypography.footnote1r.copyWith(color: descriptionColor),
),
],
),
),
);
}
}
/// Metadata for styling order type cards based on their ID.
class _OrderTypeUiMetadata {
const _OrderTypeUiMetadata({
required this.icon,
required this.backgroundColor,
@@ -228,65 +154,80 @@ class _OrderTypeUiMetadata {
required this.descriptionColor,
});
factory _OrderTypeUiMetadata.fromId(String id) {
/// Factory to get metadata based on order type ID.
factory _OrderTypeUiMetadata.fromId({required String id}) {
switch (id) {
case 'rapid':
return const _OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: Color(0xFFFFF7ED),
borderColor: Color(0xFFFFEDD5),
iconBackgroundColor: Color(0xFFF97316),
iconColor: Colors.white,
textColor: Color(0xFF9A3412),
descriptionColor: Color(0xFFC2410C),
backgroundColor: UiColors.tagPending,
borderColor: UiColors.separatorSpecial,
iconBackgroundColor: UiColors.textWarning,
iconColor: UiColors.white,
textColor: UiColors.textWarning,
descriptionColor: UiColors.textWarning,
);
case 'one-time':
return const _OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: Color(0xFFF0F9FF),
borderColor: Color(0xFFE0F2FE),
iconBackgroundColor: Color(0xFF0EA5E9),
iconColor: Colors.white,
textColor: Color(0xFF075985),
descriptionColor: Color(0xFF0369A1),
backgroundColor: UiColors.tagInProgress,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
case 'recurring':
return const _OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: Color(0xFFF0FDF4),
borderColor: Color(0xFFDCFCE7),
iconBackgroundColor: Color(0xFF22C55E),
iconColor: Colors.white,
textColor: Color(0xFF166534),
descriptionColor: Color(0xFF15803D),
backgroundColor: UiColors.tagSuccess,
borderColor: UiColors.switchActive,
iconBackgroundColor: UiColors.textSuccess,
iconColor: UiColors.white,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
);
case 'permanent':
return const _OrderTypeUiMetadata(
icon: UiIcons.briefcase,
backgroundColor: Color(0xFFF5F3FF),
borderColor: Color(0xFFEDE9FE),
iconBackgroundColor: Color(0xFF8B5CF6),
iconColor: Colors.white,
textColor: Color(0xFF5B21B6),
descriptionColor: Color(0xFF6D28D9),
backgroundColor: UiColors.tagRefunded,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
default:
return const _OrderTypeUiMetadata(
icon: UiIcons.help,
backgroundColor: Colors.grey,
borderColor: Colors.grey,
iconBackgroundColor: Colors.grey,
iconColor: Colors.white,
textColor: Colors.black,
descriptionColor: Colors.black54,
backgroundColor: UiColors.bgSecondary,
borderColor: UiColors.border,
iconBackgroundColor: UiColors.iconSecondary,
iconColor: UiColors.white,
textColor: UiColors.textPrimary,
descriptionColor: UiColors.textSecondary,
);
}
}
/// Icon for the order type.
final IconData icon;
/// Background color for the card.
final Color backgroundColor;
/// Border color for the card.
final Color borderColor;
/// Background color for the icon.
final Color iconBackgroundColor;
/// Color for the icon.
final Color iconColor;
/// Color for the title text.
final Color textColor;
/// Color for the description text.
final Color descriptionColor;
}

View File

@@ -3,14 +3,20 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/one_time_order_bloc.dart';
import '../blocs/one_time_order_event.dart';
import '../blocs/one_time_order_state.dart';
import '../widgets/one_time_order/one_time_order_date_picker.dart';
import '../widgets/one_time_order/one_time_order_location_input.dart';
import '../widgets/one_time_order/one_time_order_position_card.dart';
import '../widgets/one_time_order/one_time_order_section_header.dart';
import '../widgets/one_time_order/one_time_order_success_view.dart';
/// One-Time Order Page - Single event or shift request
/// Page for creating a one-time staffing order.
/// Users can specify the date, location, and multiple staff positions required.
class OneTimeOrderPage extends StatelessWidget {
/// Creates a [OneTimeOrderPage].
const OneTimeOrderPage({super.key});
@override
@@ -33,25 +39,19 @@ class _OneTimeOrderView extends StatelessWidget {
return BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
builder: (BuildContext context, OneTimeOrderState state) {
if (state.status == OneTimeOrderStatus.success) {
return const _SuccessView();
return OneTimeOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: 'Done',
onDone: () => Modular.to.pop(),
);
}
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft,
color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: Stack(
children: <Widget>[
@@ -84,11 +84,10 @@ class _OneTimeOrderForm extends StatelessWidget {
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
_SectionHeader(title: labels.create_your_order),
OneTimeOrderSectionHeader(title: labels.create_your_order),
const SizedBox(height: UiConstants.space4),
// Date Picker Field
_DatePickerField(
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
@@ -97,8 +96,7 @@ class _OneTimeOrderForm extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
// Location Field
_LocationField(
OneTimeOrderLocationInput(
label: labels.location_label,
value: state.location,
onChanged: (String location) =>
@@ -107,7 +105,7 @@ class _OneTimeOrderForm extends StatelessWidget {
),
const SizedBox(height: UiConstants.space6),
_SectionHeader(
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
@@ -124,10 +122,25 @@ class _OneTimeOrderForm extends StatelessWidget {
final OneTimeOrderPosition position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: _PositionCard(
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,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index, updated),
);
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index));
},
),
);
}),
@@ -137,403 +150,6 @@ class _OneTimeOrderForm extends StatelessWidget {
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.title,
this.actionLabel,
this.onAction,
});
final String title;
final String? actionLabel;
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.icon(
onPressed: onAction,
icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary),
label: Text(actionLabel!, style: UiTypography.body2b.textPrimary),
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space2),
),
),
],
);
}
}
class _DatePickerField extends StatelessWidget {
const _DatePickerField({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final DateTime value;
final ValueChanged<DateTime> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: value,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) onChanged(picked);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3 + 2,
),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(
DateFormat('EEEE, MMM d, yyyy').format(value),
style: UiTypography.body1r.textPrimary,
),
],
),
),
),
],
);
}
}
class _LocationField extends StatelessWidget {
const _LocationField({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final String value;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
// Simplified for now - can use a dropdown or Autocomplete
TextField(
controller: TextEditingController(text: value)
..selection = TextSelection.collapsed(offset: value.length),
onChanged: onChanged,
decoration: InputDecoration(
hintText: 'Select Branch/Location',
prefixIcon: const Icon(UiIcons.mapPin,
size: 20, color: UiColors.iconSecondary),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
),
style: UiTypography.body1r.textPrimary,
),
],
);
}
}
class _PositionCard extends StatelessWidget {
const _PositionCard({
required this.index,
required this.position,
required this.isRemovable,
});
final int index;
final OneTimeOrderPosition position;
final bool isRemovable;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'${labels.positions_title} #${index + 1}',
style: UiTypography.body1b.textPrimary,
),
if (isRemovable)
IconButton(
icon: const Icon(UiIcons.delete,
size: 20, color: UiColors.destructive),
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
),
],
),
const Divider(height: UiConstants.space6),
// Role (Dropdown simulation)
_LabelField(
label: labels.select_role,
child: DropdownButtonFormField<String>(
initialValue: position.role.isEmpty ? null : position.role,
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
.map((String role) => DropdownMenuItem<String>(
value: role,
child:
Text(role, style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (String? val) {
if (val != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(role: val)),
);
}
},
decoration: _inputDecoration(UiIcons.briefcase),
),
),
const SizedBox(height: UiConstants.space4),
// Count
_LabelField(
label: labels.workers_label,
child: Row(
children: <Widget>[
_CounterButton(
icon: UiIcons.minus,
onPressed: position.count > 1
? () => BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index,
position.copyWith(count: position.count - 1)),
)
: null,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4),
child: Text('${position.count}',
style: UiTypography.headline3m.textPrimary),
),
_CounterButton(
icon: UiIcons.add,
onPressed: () =>
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(count: position.count + 1)),
),
),
],
),
),
const SizedBox(height: UiConstants.space4),
// Start/End Time
Row(
children: <Widget>[
Expanded(
child: _LabelField(
label: labels.start_label,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 9, minute: 0),
);
if (picked != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index,
position.copyWith(
startTime: picked.format(context)),
),
);
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.startTime.isEmpty
? '--:--'
: position.startTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _LabelField(
label: labels.end_label,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 17, minute: 0),
);
if (picked != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index,
position.copyWith(endTime: picked.format(context)),
),
);
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.endTime.isEmpty ? '--:--' : position.endTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Lunch Break
_LabelField(
label: labels.lunch_break_label,
child: DropdownButtonFormField<int>(
initialValue: position.lunchBreak,
items: <int>[0, 30, 45, 60]
.map((int mins) => DropdownMenuItem<int>(
value: mins,
child: Text('${mins}m',
style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (int? val) {
if (val != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(lunchBreak: val)),
);
}
},
decoration: _inputDecoration(UiIcons.clock),
),
),
],
),
);
}
InputDecoration _inputDecoration(IconData icon) => InputDecoration(
prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary),
contentPadding:
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
);
BoxDecoration _boxDecoration() => BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
);
}
class _LabelField extends StatelessWidget {
const _LabelField({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space1),
child,
],
);
}
}
class _CounterButton extends StatelessWidget {
const _CounterButton({required this.icon, this.onPressed});
final IconData icon;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border.all(
color: onPressed != null
? UiColors.border
: UiColors.border.withOpacity(0.5)),
borderRadius: UiConstants.radiusLg,
color: onPressed != null ? UiColors.white : UiColors.background,
),
child: Icon(
icon,
size: 16,
color: onPressed != null
? UiColors.iconPrimary
: UiColors.iconSecondary.withOpacity(0.5),
),
),
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
@@ -563,87 +179,30 @@ class _BottomActionButton extends StatelessWidget {
),
],
),
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
),
elevation: 0,
),
child: isLoading
? const SizedBox(
child: isLoading
? const UiButton(
buttonBuilder: _dummyBuilder,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: UiColors.white, strokeWidth: 2),
)
: Text(label,
style: UiTypography.body1b.copyWith(color: UiColors.white)),
),
color: UiColors.primary, strokeWidth: 2),
),
)
: UiButton.primary(
text: label,
onPressed: onPressed,
size: UiButtonSize.large,
),
);
}
}
class _SuccessView extends StatelessWidget {
const _SuccessView();
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return Scaffold(
backgroundColor: UiColors.white,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: UiColors.tagSuccess,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.check,
size: 50, color: UiColors.textSuccess),
),
const SizedBox(height: UiConstants.space8),
Text(
labels.success_title,
style: UiTypography.headline2m.textPrimary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space4),
Text(
labels.success_message,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space10),
ElevatedButton(
onPressed: () => Modular.to.pop(),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
),
),
child: Text('Done',
style: UiTypography.body1b.copyWith(color: UiColors.white)),
),
],
),
),
),
);
static Widget _dummyBuilder(
BuildContext context,
VoidCallback? onPressed,
ButtonStyle? style,
Widget child,
) {
return Center(child: child);
}
}

View File

@@ -3,8 +3,10 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// Permanent Order Page - Long-term staffing placement
/// Permanent Order Page - Long-term staffing placement.
/// Placeholder for future implementation.
class PermanentOrderPage extends StatelessWidget {
/// Creates a [PermanentOrderPage].
const PermanentOrderPage({super.key});
@override
@@ -13,30 +15,24 @@ class PermanentOrderPage extends StatelessWidget {
t.client_create_order.permanent;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title: Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
child: Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
),
);

View File

@@ -7,9 +7,14 @@ import 'package:intl/intl.dart';
import '../blocs/rapid_order_bloc.dart';
import '../blocs/rapid_order_event.dart';
import '../blocs/rapid_order_state.dart';
import '../widgets/rapid_order/rapid_order_example_card.dart';
import '../widgets/rapid_order/rapid_order_header.dart';
import '../widgets/rapid_order/rapid_order_success_view.dart';
/// Rapid Order Flow Page - Emergency staffing requests
/// Rapid Order Flow Page - Emergency staffing requests.
/// Features voice recognition simulation and quick example selection.
class RapidOrderPage extends StatelessWidget {
/// Creates a [RapidOrderPage].
const RapidOrderPage({super.key});
@override
@@ -26,10 +31,18 @@ class _RapidOrderView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
return BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderSuccess) {
return const _SuccessView();
return RapidOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pop(),
);
}
return const _RapidOrderForm();
@@ -56,7 +69,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid;
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
final DateTime now = DateTime.now();
final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now);
final String timeStr = DateFormat('h:mm a').format(now);
@@ -73,97 +87,15 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
}
},
child: Scaffold(
backgroundColor: UiColors.background,
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
// Header with gradient
Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Icon(
UiIcons.zap,
color: UiColors.accent,
size: 18,
),
const SizedBox(width: UiConstants.space2),
Text(
labels.title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
),
],
),
Text(
labels.subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
dateStr,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
Text(
timeStr,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
],
),
],
),
RapidOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
date: dateStr,
time: timeStr,
onBack: () => Modular.to.pop(),
),
// Content
@@ -212,40 +144,13 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
builder: (BuildContext context, RapidOrderState state) {
final RapidOrderInitial? initialState =
state is RapidOrderInitial ? state : null;
final bool isSubmitting = state is RapidOrderSubmitting;
final bool isSubmitting =
state is RapidOrderSubmitting;
return Column(
children: <Widget>[
// Icon
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive
.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.destructive
.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
),
_AnimatedZapIcon(),
const SizedBox(height: UiConstants.space4),
Text(
labels.need_staff,
@@ -267,51 +172,21 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
.map((MapEntry<int, String> entry) {
final int index = entry.key;
final String example = entry.value;
final bool isFirst = index == 0;
final bool isHighlighted = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2),
child: GestureDetector(
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: isFirst
? UiColors.accent
.withValues(alpha: 0.15)
: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: isFirst
? UiColors.accent
: UiColors.border,
),
),
child: RichText(
text: TextSpan(
style:
UiTypography.body2r.textPrimary,
children: <InlineSpan>[
TextSpan(
text: labels.example,
style: UiTypography
.body2b.textPrimary,
),
TextSpan(text: example),
],
),
),
),
),
);
}),
@@ -332,13 +207,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
color: UiColors.textPlaceholder,
),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: const BorderSide(
color: UiColors.border,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(
color: UiColors.border,
),
@@ -350,100 +219,12 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
const SizedBox(height: UiConstants.space4),
// Actions
Row(
children: <Widget>[
Expanded(
child: SizedBox(
height: 52,
child: OutlinedButton.icon(
onPressed: initialState != null
? () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
const RapidOrderVoiceToggled(),
)
: null,
icon: Icon(
UiIcons
.bell, // Using bell as mic placeholder
size: 20,
color:
initialState?.isListening == true
? UiColors.destructive
: UiColors.iconPrimary,
),
label: Text(
initialState?.isListening == true
? labels.listening
: labels.speak,
style: UiTypography.body2b.copyWith(
color: initialState?.isListening ==
true
? UiColors.destructive
: UiColors.textPrimary,
),
),
style: OutlinedButton.styleFrom(
backgroundColor:
initialState?.isListening == true
? UiColors.destructive
.withValues(alpha: 0.05)
: UiColors.white,
side: BorderSide(
color: initialState?.isListening ==
true
? UiColors.destructive
: UiColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: SizedBox(
height: 52,
child: ElevatedButton.icon(
onPressed: isSubmitting ||
(initialState?.message
.trim()
.isEmpty ??
true)
? null
: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
const RapidOrderSubmitted(),
),
icon: const Icon(
UiIcons.arrowRight,
size: 20,
color: UiColors.white,
),
label: Text(
isSubmitting
? labels.sending
: labels.send,
style: UiTypography.body2b.copyWith(
color: UiColors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
elevation: 0,
),
),
),
),
],
_RapidOrderActions(
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty: initialState != null &&
initialState.message.trim().isEmpty,
),
],
);
@@ -461,102 +242,85 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
}
}
class _SuccessView extends StatelessWidget {
const _SuccessView();
class _AnimatedZapIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid;
return Scaffold(
body: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
UiColors.primary,
UiColors.primary.withValues(alpha: 0.85),
],
),
return Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
child: SafeArea(
child: Center(
child: Container(
margin:
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: UiColors.accent,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
UiIcons.zap,
color: UiColors.textPrimary,
size: 32,
),
),
),
const SizedBox(height: UiConstants.space6),
Text(
labels.success_title,
style: UiTypography.headline1m.textPrimary,
),
const SizedBox(height: UiConstants.space3),
Text(
labels.success_message,
textAlign: TextAlign.center,
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: UiConstants.space8),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () => Modular.to.pop(),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.textPrimary,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
elevation: 0,
),
child: Text(
labels.back_to_orders,
style: UiTypography.body1b.copyWith(
color: UiColors.white,
),
),
),
),
],
),
),
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.destructive.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
);
}
}
class _RapidOrderActions extends StatelessWidget {
const _RapidOrderActions({
required this.labels,
required this.isSubmitting,
required this.isListening,
required this.isMessageEmpty,
});
final TranslationsClientCreateOrderRapidEn labels;
final bool isSubmitting;
final bool isListening;
final bool isMessageEmpty;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderVoiceToggled(),
),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
: null,
side: isListening
? const BorderSide(color: UiColors.destructive)
: null,
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
text: isSubmitting ? labels.sending : labels.send,
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderSubmitted(),
),
),
),
],
);
}
}

View File

@@ -3,8 +3,10 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// Recurring Order Page - Ongoing weekly/monthly coverage
/// Recurring Order Page - Ongoing weekly/monthly coverage.
/// Placeholder for future implementation.
class RecurringOrderPage extends StatelessWidget {
/// Creates a [RecurringOrderPage].
const RecurringOrderPage({super.key});
@override
@@ -13,30 +15,24 @@ class RecurringOrderPage extends StatelessWidget {
t.client_create_order.recurring;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title: Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
child: Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
),
);

View File

@@ -0,0 +1,68 @@
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.
class OneTimeOrderDatePicker extends StatelessWidget {
/// 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;
/// Creates a [OneTimeOrderDatePicker].
const OneTimeOrderDatePicker({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: value,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
onChanged(picked);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3 + 2,
),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(
DateFormat('EEEE, MMM d, yyyy').format(value),
style: UiTypography.body1r.textPrimary,
),
],
),
),
),
],
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A location input field for the one-time order form.
class OneTimeOrderLocationInput extends StatelessWidget {
/// The label text to display above the field.
final String label;
/// The current location value.
final String value;
/// Callback when the location text changes.
final ValueChanged<String> onChanged;
/// Creates a [OneTimeOrderLocationInput].
const OneTimeOrderLocationInput({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return UiTextField(
label: label,
hintText: 'Select Branch/Location',
controller: TextEditingController(text: value)
..selection = TextSelection.collapsed(offset: value.length),
onChanged: onChanged,
prefixIcon: UiIcons.mapPin,
);
}
}

View File

@@ -0,0 +1,294 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// A card widget for editing a specific position in a one-time order.
class OneTimeOrderPositionCard extends StatelessWidget {
/// 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;
/// 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,
super.key,
});
@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),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'$positionLabel #${index + 1}',
style: UiTypography.body1b.textPrimary,
),
if (isRemovable)
IconButton(
icon: const Icon(UiIcons.delete,
size: 20, color: UiColors.destructive),
onPressed: onRemoved,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
),
],
),
const Divider(height: UiConstants.space6),
// Role (Dropdown)
_LabelField(
label: roleLabel,
child: DropdownButtonFormField<String>(
value: position.role.isEmpty ? null : position.role,
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
.map((String role) => DropdownMenuItem<String>(
value: role,
child:
Text(role, style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (String? val) {
if (val != null) {
onUpdated(position.copyWith(role: val));
}
},
decoration: _inputDecoration(UiIcons.briefcase),
),
),
const SizedBox(height: UiConstants.space4),
// Count (Counter)
_LabelField(
label: workersLabel,
child: Row(
children: <Widget>[
_CounterButton(
icon: UiIcons.minus,
onPressed: position.count > 1
? () => onUpdated(
position.copyWith(count: position.count - 1))
: null,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4),
child: Text('${position.count}',
style: UiTypography.headline3m.textPrimary),
),
_CounterButton(
icon: UiIcons.add,
onPressed: () =>
onUpdated(position.copyWith(count: position.count + 1)),
),
],
),
),
const SizedBox(height: UiConstants.space4),
// Start/End Time
Row(
children: <Widget>[
Expanded(
child: _LabelField(
label: startLabel,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 9, minute: 0),
);
if (picked != null) {
onUpdated(position.copyWith(
startTime: picked.format(context)));
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.startTime.isEmpty
? '--:--'
: position.startTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _LabelField(
label: endLabel,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 17, minute: 0),
);
if (picked != null) {
onUpdated(
position.copyWith(endTime: picked.format(context)));
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.endTime.isEmpty ? '--:--' : position.endTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Lunch Break
_LabelField(
label: lunchLabel,
child: DropdownButtonFormField<int>(
value: position.lunchBreak,
items: <int>[0, 30, 45, 60]
.map((int mins) => DropdownMenuItem<int>(
value: mins,
child: Text('${mins}m',
style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (int? val) {
if (val != null) {
onUpdated(position.copyWith(lunchBreak: val));
}
},
decoration: _inputDecoration(UiIcons.clock),
),
),
],
),
);
}
InputDecoration _inputDecoration(IconData icon) => InputDecoration(
prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary),
contentPadding:
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
);
BoxDecoration _boxDecoration() => BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
);
}
class _LabelField extends StatelessWidget {
const _LabelField({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space1),
child,
],
);
}
}
class _CounterButton extends StatelessWidget {
const _CounterButton({required this.icon, this.onPressed});
final IconData icon;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border.all(
color: onPressed != null
? UiColors.border
: UiColors.border.withOpacity(0.5)),
borderRadius: UiConstants.radiusLg,
color: onPressed != null ? UiColors.white : UiColors.background,
),
child: Icon(
icon,
size: 16,
color: onPressed != null
? UiColors.iconPrimary
: UiColors.iconSecondary.withOpacity(0.5),
),
),
);
}
}

View File

@@ -0,0 +1,42 @@
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 {
/// 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;
/// Creates a [OneTimeOrderSectionHeader].
const OneTimeOrderSectionHeader({
required this.title,
this.actionLabel,
this.onAction,
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(title, style: UiTypography.headline4m.textPrimary),
if (actionLabel != null && onAction != null)
TextButton.icon(
onPressed: onAction,
icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary),
label: Text(actionLabel!, style: UiTypography.body2b.textPrimary),
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space2),
),
),
],
);
}
}

View File

@@ -0,0 +1,71 @@
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.
class OneTimeOrderSuccessView extends StatelessWidget {
/// 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;
/// Creates a [OneTimeOrderSuccessView].
const OneTimeOrderSuccessView({
required this.title,
required this.message,
required this.buttonLabel,
required this.onDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.white,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: UiColors.tagSuccess,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.check,
size: 50, color: UiColors.textSuccess),
),
const SizedBox(height: UiConstants.space8),
Text(
title,
style: UiTypography.headline2m.textPrimary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space4),
Text(
message,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space10),
UiButton.primary(
text: buttonLabel,
onPressed: onDone,
size: UiButtonSize.large,
),
],
),
),
),
);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,122 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the rapid order flow with a gradient background.
class RapidOrderHeader extends StatelessWidget {
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// The formatted current date.
final String date;
/// The formatted current time.
final String time;
/// Callback when the back button is pressed.
final VoidCallback onBack;
/// Creates a [RapidOrderHeader].
const RapidOrderHeader({
required this.title,
required this.subtitle,
required this.date,
required this.time,
required this.onBack,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: onBack,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Icon(
UiIcons.zap,
color: UiColors.accent,
size: 18,
),
const SizedBox(width: UiConstants.space2),
Text(
title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
),
],
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
date,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
Text(
time,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
],
),
],
),
);
}
}

View File

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