diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 700570ef..d207dc0b 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -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)" } } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart index b5623471..42c91202 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart @@ -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: [ - 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; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart index b65bab3f..96995b2e 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -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( 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: [ @@ -84,11 +84,10 @@ class _OneTimeOrderForm extends StatelessWidget { return ListView( padding: const EdgeInsets.all(UiConstants.space5), children: [ - _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(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(context).add( + OneTimeOrderPositionUpdated(index, updated), + ); + }, + onRemoved: () { + BlocProvider.of(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: [ - 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 onChanged; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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: [ - 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 onChanged; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - 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(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( - initialValue: position.role.isEmpty ? null : position.role, - items: ['Server', 'Bartender', 'Cook', 'Busser', 'Host'] - .map((String role) => DropdownMenuItem( - value: role, - child: - Text(role, style: UiTypography.body1r.textPrimary), - )) - .toList(), - onChanged: (String? val) { - if (val != null) { - BlocProvider.of(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: [ - _CounterButton( - icon: UiIcons.minus, - onPressed: position.count > 1 - ? () => BlocProvider.of(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(context).add( - OneTimeOrderPositionUpdated( - index, position.copyWith(count: position.count + 1)), - ), - ), - ], - ), - ), - const SizedBox(height: UiConstants.space4), - - // Start/End Time - Row( - children: [ - 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(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(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( - initialValue: position.lunchBreak, - items: [0, 30, 45, 60] - .map((int mins) => DropdownMenuItem( - value: mins, - child: Text('${mins}m', - style: UiTypography.body1r.textPrimary), - )) - .toList(), - onChanged: (int? val) { - if (val != null) { - BlocProvider.of(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: [ - 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: [ - 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); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index 656ecdb1..fd38a142 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -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: [ - Text( - labels.subtitle, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - ], + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ), ); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart index 0a269b3f..0f0bb874 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart @@ -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( 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: [ - // 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: [ - UiColors.destructive, - UiColors.destructive.withValues(alpha: 0.85), - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - 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: [ - Row( - children: [ - 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: [ - 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: [ // Icon - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.destructive, - UiColors.destructive - .withValues(alpha: 0.85), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: UiConstants.radiusLg, - 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 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( 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: [ - 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: [ - Expanded( - child: SizedBox( - height: 52, - child: OutlinedButton.icon( - onPressed: initialState != null - ? () => - BlocProvider.of( - 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( - 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: [ - UiColors.primary, - UiColors.primary.withValues(alpha: 0.85), - ], - ), + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + 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( - color: UiColors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - 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( + 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: [ + Expanded( + child: UiButton.secondary( + text: isListening ? labels.listening : labels.speak, + leadingIcon: UiIcons.bell, // Placeholder for mic + onPressed: () => BlocProvider.of(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(context).add( + const RapidOrderSubmitted(), + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index db437f9d..64324b46 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -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: [ - Text( - labels.subtitle, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - ], + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ), ); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart new file mode 100644 index 00000000..5b32274d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart @@ -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 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: [ + 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: [ + 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, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart new file mode 100644 index 00000000..3f93da9d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart @@ -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 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, + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart new file mode 100644 index 00000000..a605ea5c --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart @@ -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 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( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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( + value: position.role.isEmpty ? null : position.role, + items: ['Server', 'Bartender', 'Cook', 'Busser', 'Host'] + .map((String role) => DropdownMenuItem( + 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: [ + _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: [ + 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( + value: position.lunchBreak, + items: [0, 30, 45, 60] + .map((int mins) => DropdownMenuItem( + 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: [ + 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), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart new file mode 100644 index 00000000..29c8df31 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart @@ -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: [ + 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), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart new file mode 100644 index 00000000..ea704758 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart @@ -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: [ + 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, + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart new file mode 100644 index 00000000..8b450b99 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart @@ -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: [ + 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart new file mode 100644 index 00000000..3bde4479 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart @@ -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: [ + TextSpan( + text: label, + style: UiTypography.body2b.textPrimary, + ), + TextSpan(text: ' $example'), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart new file mode 100644 index 00000000..4d7a3848 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart @@ -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: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + 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: [ + Row( + children: [ + 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: [ + 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), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart new file mode 100644 index 00000000..3ea9ad4d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart @@ -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: [ + 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( + color: UiColors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +}