feat: complete client reports and hub management UI, comment out export buttons

This commit is contained in:
2026-02-19 13:00:48 +05:30
parent 1ca3f714c8
commit c4610003b4
11 changed files with 533 additions and 1 deletions

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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(),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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> {
),
),
),
*/
],
),
],

View File

@@ -132,6 +132,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
),
],
),
/*
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
@@ -176,6 +177,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
),
),
),
*/
],
),
),

View File

@@ -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> {
),
),
),
*/
],
),
),

View File

@@ -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> {
),
),
),
*/
],
),
),

View File

@@ -110,6 +110,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
),
],
),
/*
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
@@ -154,6 +155,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
),
),
),
*/
],
),
),

View File

@@ -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(),
),
],
),
);
}
}

View File

@@ -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';