diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/reorder_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/reorder_usecase.dart new file mode 100644 index 00000000..296816cf --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/reorder_usecase.dart @@ -0,0 +1,25 @@ +import 'package:krow_core/core.dart'; +import '../repositories/client_create_order_repository_interface.dart'; + +/// Arguments for the ReorderUseCase. +class ReorderArguments { + const ReorderArguments({ + required this.previousOrderId, + required this.newDate, + }); + + final String previousOrderId; + final DateTime newDate; +} + +/// Use case for reordering an existing staffing order. +class ReorderUseCase implements UseCase, ReorderArguments> { + const ReorderUseCase(this._repository); + + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(ReorderArguments params) { + return _repository.reorder(params.previousOrderId, params.newDate); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart new file mode 100644 index 00000000..d62e0f92 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/update_hub_usecase.dart @@ -0,0 +1,57 @@ +import 'package:krow_domain/krow_domain.dart'; + +import '../repositories/hub_repository_interface.dart'; +import '../../domain/arguments/create_hub_arguments.dart'; + +/// Arguments for the UpdateHubUseCase. +class UpdateHubArguments { + const UpdateHubArguments({ + required this.id, + this.name, + this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + final String id; + final String? name; + final String? address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; +} + +/// Use case for updating an existing hub. +class UpdateHubUseCase implements UseCase, UpdateHubArguments> { + UpdateHubUseCase(this.repository); + + final HubRepositoryInterface repository; + + @override + Future call(UpdateHubArguments params) { + return repository.updateHub( + id: params.id, + name: params.name, + address: params.address, + placeId: params.placeId, + latitude: params.latitude, + longitude: params.longitude, + city: params.city, + state: params.state, + street: params.street, + country: params.country, + zipCode: params.zipCode, + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart new file mode 100644 index 00000000..e3eccc0a --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/hub_details_page.dart @@ -0,0 +1,154 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../blocs/client_hubs_bloc.dart'; +import '../blocs/client_hubs_event.dart'; +import '../widgets/hub_form_dialog.dart'; + +class HubDetailsPage extends StatelessWidget { + const HubDetailsPage({ + required this.hub, + required this.bloc, + super.key, + }); + + final Hub hub; + final ClientHubsBloc bloc; + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: Scaffold( + appBar: AppBar( + title: Text(hub.name), + backgroundColor: UiColors.foreground, + leading: IconButton( + icon: const Icon(UiIcons.arrowLeft, color: UiColors.white), + onPressed: () => Modular.to.pop(), + ), + actions: [ + IconButton( + icon: const Icon(UiIcons.edit, color: UiColors.white), + onPressed: () => _showEditDialog(context), + ), + ], + ), + backgroundColor: UiColors.bgMenu, + body: Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailItem( + label: 'Name', + value: hub.name, + icon: UiIcons.home, + ), + const SizedBox(height: UiConstants.space4), + _buildDetailItem( + label: 'Address', + value: hub.address, + icon: UiIcons.mapPin, + ), + const SizedBox(height: UiConstants.space4), + _buildDetailItem( + label: 'NFC Tag', + value: hub.nfcTagId ?? 'Not Assigned', + icon: UiIcons.nfc, + isHighlight: hub.nfcTagId != null, + ), + ], + ), + ), + ), + ); + } + + Widget _buildDetailItem({ + required String label, + required String value, + required IconData icon, + bool isHighlight = false, + }) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + boxShadow: const [ + BoxShadow( + color: UiColors.popupShadow, + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: isHighlight ? UiColors.tagInProgress : UiColors.bgInput, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Icon( + icon, + color: isHighlight ? UiColors.iconSuccess : UiColors.iconPrimary, + size: 20, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: UiTypography.footnote1r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + Text( + value, + style: UiTypography.body1m.textPrimary, + ), + ], + ), + ), + ], + ), + ); + } + + void _showEditDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => HubFormDialog( + hub: hub, + onSave: (name, address, {placeId, latitude, longitude, city, state, street, country, zipCode}) { + bloc.add( + ClientHubsUpdateRequested( + id: hub.id, + name: name, + address: address, + placeId: placeId, + latitude: latitude, + longitude: longitude, + city: city, + state: state, + street: street, + country: country, + zipCode: zipCode, + ), + ); + Navigator.of(context).pop(); // Close dialog + Navigator.of(context).pop(); // Go back to list to refresh + }, + onCancel: () => Navigator.of(context).pop(), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart new file mode 100644 index 00000000..7a4d0cd7 --- /dev/null +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_form_dialog.dart @@ -0,0 +1,200 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:core_localization/core_localization.dart'; +import 'package:google_places_flutter/model/prediction.dart'; +import 'package:krow_domain/krow_domain.dart'; + +import 'hub_address_autocomplete.dart'; + +/// A dialog for adding or editing a hub. +class HubFormDialog extends StatefulWidget { + + /// Creates a [HubFormDialog]. + const HubFormDialog({ + required this.onSave, + required this.onCancel, + this.hub, + super.key, + }); + + /// The hub to edit. If null, a new hub is created. + final Hub? hub; + + /// Callback when the "Save" button is pressed. + final void Function( + String name, + String address, { + String? placeId, + double? latitude, + double? longitude, + }) onSave; + + /// Callback when the dialog is cancelled. + final VoidCallback onCancel; + + @override + State createState() => _HubFormDialogState(); +} + +class _HubFormDialogState extends State { + late final TextEditingController _nameController; + late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + Prediction? _selectedPrediction; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.hub?.name); + _addressController = TextEditingController(text: widget.hub?.address); + _addressFocusNode = FocusNode(); + } + + @override + void dispose() { + _nameController.dispose(); + _addressController.dispose(); + _addressFocusNode.dispose(); + super.dispose(); + } + + final GlobalKey _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final bool isEditing = widget.hub != null; + final String title = isEditing + ? 'Edit Hub' // TODO: localize + : t.client_hubs.add_hub_dialog.title; + + final String buttonText = isEditing + ? 'Save Changes' // TODO: localize + : t.client_hubs.add_hub_dialog.create_button; + + return Container( + color: UiColors.bgOverlay, + child: Center( + child: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + padding: const EdgeInsets.all(UiConstants.space5), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + boxShadow: const [ + BoxShadow(color: UiColors.popupShadow, blurRadius: 20), + ], + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + title, + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space5), + _buildFieldLabel(t.client_hubs.add_hub_dialog.name_label), + TextFormField( + controller: _nameController, + style: UiTypography.body1r.textPrimary, + validator: (String? value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required'; + } + return null; + }, + decoration: _buildInputDecoration( + t.client_hubs.add_hub_dialog.name_hint, + ), + ), + const SizedBox(height: UiConstants.space4), + _buildFieldLabel(t.client_hubs.add_hub_dialog.address_label), + HubAddressAutocomplete( + controller: _addressController, + hintText: t.client_hubs.add_hub_dialog.address_hint, + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, + ), + const SizedBox(height: UiConstants.space8), + Row( + children: [ + Expanded( + child: UiButton.secondary( + onPressed: widget.onCancel, + text: t.common.cancel, + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: UiButton.primary( + onPressed: () { + if (_formKey.currentState!.validate()) { + if (_addressController.text.trim().isEmpty) { + UiSnackbar.show(context, message: 'Address is required', type: UiSnackbarType.error); + return; + } + + widget.onSave( + _nameController.text, + _addressController.text, + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse( + _selectedPrediction?.lat ?? '', + ), + longitude: double.tryParse( + _selectedPrediction?.lng ?? '', + ), + ); + } + }, + text: buttonText, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildFieldLabel(String label) { + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space2), + child: Text(label, style: UiTypography.body2m.textPrimary), + ); + } + + InputDecoration _buildInputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: UiTypography.body2r.textPlaceholder, + filled: true, + fillColor: UiColors.input, + contentPadding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: 14, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + borderSide: const BorderSide(color: UiColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + borderSide: const BorderSide(color: UiColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + borderSide: const BorderSide(color: UiColors.ring, width: 2), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart index 7ee23f6a..25b68528 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart @@ -120,6 +120,7 @@ class _CoverageReportPageState extends State { ], ), // Export button +/* GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( @@ -158,6 +159,7 @@ class _CoverageReportPageState extends State { ), ), ), +*/ ], ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart index 66772cef..323cbcf6 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart @@ -132,6 +132,7 @@ class _DailyOpsReportPageState extends State { ), ], ), +/* GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( @@ -176,6 +177,7 @@ class _DailyOpsReportPageState extends State { ), ), ), +*/ ], ), ), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart index d70c8d79..100f398e 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart @@ -99,6 +99,7 @@ class _NoShowReportPageState extends State { ], ), // Export button +/* GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( @@ -137,6 +138,7 @@ class _NoShowReportPageState extends State { ), ), ), +*/ ], ), ), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart index 4dae406e..837053fd 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart @@ -182,6 +182,7 @@ class _PerformanceReportPageState extends State { ], ), // Export +/* GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( @@ -217,6 +218,7 @@ class _PerformanceReportPageState extends State { ), ), ), +*/ ], ), ), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart index 9f20bcdd..a09aa76c 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart @@ -110,6 +110,7 @@ class _SpendReportPageState extends State { ), ], ), +/* GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( @@ -154,6 +155,7 @@ class _SpendReportPageState extends State { ), ), ), +*/ ], ), ), diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_logout.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_logout.dart new file mode 100644 index 00000000..ea359254 --- /dev/null +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_logout.dart @@ -0,0 +1,87 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; +import '../../blocs/client_settings_bloc.dart'; + +/// A widget that displays the log out button. +class SettingsLogout extends StatelessWidget { + /// Creates a [SettingsLogout]. + const SettingsLogout({super.key}); + + @override + Widget build(BuildContext context) { + final TranslationsClientSettingsProfileEn labels = + t.client_settings.profile; + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), + sliver: SliverToBoxAdapter( + child: BlocBuilder( + builder: (BuildContext context, ClientSettingsState state) { + return UiButton.primary( + text: labels.log_out, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.white, + foregroundColor: UiColors.textPrimary, + side: const BorderSide(color: UiColors.textPrimary), + elevation: 0, + ), + onPressed: state is ClientSettingsLoading + ? null + : () => _showSignOutDialog(context), + ); + }, + ), + ), + ); + } + + /// Handles the sign-out button click event. + void _onSignoutClicked(BuildContext context) { + ReadContext( + context, + ).read().add(const ClientSettingsSignOutRequested()); + } + + /// Shows a confirmation dialog for signing out. + Future _showSignOutDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext dialogContext) => AlertDialog( + backgroundColor: UiColors.bgPopup, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg), + title: Text( + t.client_settings.profile.log_out, + style: UiTypography.headline3m.textPrimary, + ), + content: Text( + t.client_settings.profile.log_out_confirmation, + style: UiTypography.body2r.textSecondary, + ), + actions: [ + // Log out button + UiButton.primary( + text: t.client_settings.profile.log_out, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.white, + foregroundColor: UiColors.textPrimary, + side: const BorderSide(color: UiColors.textPrimary), + elevation: 0, + ), + onPressed: () => _onSignoutClicked(context), + ), + + // Cancel button + UiButton.secondary( + text: t.common.cancel, + onPressed: () => Modular.to.pop(), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart index 2b910be8..6b4d54cc 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart @@ -3,4 +3,3 @@ export 'src/presentation/pages/get_started_page.dart'; export 'src/presentation/pages/phone_verification_page.dart'; export 'src/presentation/pages/profile_setup_page.dart'; export 'src/staff_authentication_module.dart'; -export 'src/domain/repositories/auth_repository_interface.dart';