feat: complete client reports and hub management UI, comment out export buttons
This commit is contained in:
@@ -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<Future<void>, ReorderArguments> {
|
||||
const ReorderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<void> call(ReorderArguments params) {
|
||||
return _repository.reorder(params.previousOrderId, params.newDate);
|
||||
}
|
||||
}
|
||||
@@ -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<Future<Hub>, UpdateHubArguments> {
|
||||
UpdateHubUseCase(this.repository);
|
||||
|
||||
final HubRepositoryInterface repository;
|
||||
|
||||
@override
|
||||
Future<Hub> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<ClientHubsBloc>.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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<HubFormDialog> createState() => _HubFormDialogState();
|
||||
}
|
||||
|
||||
class _HubFormDialogState extends State<HubFormDialog> {
|
||||
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<FormState> _formKey = GlobalKey<FormState>();
|
||||
|
||||
@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>[
|
||||
BoxShadow(color: UiColors.popupShadow, blurRadius: 20),
|
||||
],
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
||||
],
|
||||
),
|
||||
// Export button
|
||||
/*
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -158,6 +159,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -132,6 +132,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
/*
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -176,6 +177,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -99,6 +99,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
],
|
||||
),
|
||||
// Export button
|
||||
/*
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -137,6 +138,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -182,6 +182,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
],
|
||||
),
|
||||
// Export
|
||||
/*
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -217,6 +218,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -110,6 +110,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
/*
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -154,6 +155,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<ClientSettingsBloc, ClientSettingsState>(
|
||||
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<ClientSettingsBloc>().add(const ClientSettingsSignOutRequested());
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog for signing out.
|
||||
Future<void> _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: <Widget>[
|
||||
// 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user