refactor: Refactor hub form from a dialog to a dedicated widget and streamline HubDetailsPage UI and bloc initialization.

This commit is contained in:
Achintha Isuru
2026-02-26 13:39:33 -05:00
parent 1a13ea70a3
commit 94e15ae05d
5 changed files with 407 additions and 466 deletions

View File

@@ -56,10 +56,8 @@ class ClientHubsModule extends Module {
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubDetails),
child: (_) {
final Map<String, dynamic> data = r.args.data as Map<String, dynamic>;
return HubDetailsPage(
hub: data['hub'] as Hub,
bloc: Modular.get<HubDetailsBloc>(),
);
final Hub hub = data['hub'] as Hub;
return HubDetailsPage(hub: hub);
},
);
r.child(
@@ -67,21 +65,19 @@ class ClientHubsModule extends Module {
transition: TransitionType.custom,
customTransition: CustomTransition(
opaque: false,
transitionBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(opacity: animation, child: child);
},
transitionBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(opacity: animation, child: child);
},
),
child: (_) {
final Map<String, dynamic> data = r.args.data as Map<String, dynamic>;
return EditHubPage(
hub: data['hub'] as Hub?,
bloc: Modular.get<EditHubBloc>(),
);
return EditHubPage(hub: data['hub'] as Hub?);
},
);
}

View File

@@ -8,32 +8,21 @@ import 'package:krow_domain/krow_domain.dart';
import '../blocs/edit_hub/edit_hub_bloc.dart';
import '../blocs/edit_hub/edit_hub_event.dart';
import '../blocs/edit_hub/edit_hub_state.dart';
import '../widgets/hub_form_dialog.dart';
import '../widgets/hub_form.dart';
/// A wrapper page that shows the hub form in a modal-style layout.
class EditHubPage extends StatefulWidget {
const EditHubPage({this.hub, required this.bloc, super.key});
class EditHubPage extends StatelessWidget {
const EditHubPage({this.hub, super.key});
final Hub? hub;
final EditHubBloc bloc;
@override
State<EditHubPage> createState() => _EditHubPageState();
}
class _EditHubPageState extends State<EditHubPage> {
@override
void initState() {
super.initState();
// Load available cost centers
widget.bloc.add(const EditHubCostCentersLoadRequested());
}
@override
Widget build(BuildContext context) {
return BlocProvider<EditHubBloc>.value(
value: widget.bloc,
child: BlocListener<EditHubBloc, EditHubState>(
return BlocProvider<EditHubBloc>(
create: (_) =>
Modular.get<EditHubBloc>()
..add(const EditHubCostCentersLoadRequested()),
child: BlocConsumer<EditHubBloc, EditHubState>(
listenWhen: (EditHubState prev, EditHubState curr) =>
prev.status != curr.status || prev.successKey != curr.successKey,
listener: (BuildContext context, EditHubState state) {
@@ -58,74 +47,70 @@ class _EditHubPageState extends State<EditHubPage> {
);
}
},
child: BlocBuilder<EditHubBloc, EditHubState>(
builder: (BuildContext context, EditHubState state) {
final bool isSaving = state.status == EditHubStatus.loading;
builder: (BuildContext context, EditHubState state) {
final bool isSaving = state.status == EditHubStatus.loading;
final bool isEditing = hub != null;
final String title = isEditing
? t.client_hubs.edit_hub.title
: t.client_hubs.add_hub_dialog.title;
return Scaffold(
backgroundColor: UiColors.bgOverlay,
body: Stack(
children: <Widget>[
// Tap background to dismiss
GestureDetector(
onTap: () => Modular.to.pop(),
child: Container(color: Colors.transparent),
),
// Dialog-style content centered
Align(
alignment: Alignment.center,
child: HubFormDialog(
hub: widget.hub,
costCenters: state.costCenters,
onCancel: () => Modular.to.pop(),
onSave: ({
required String name,
required String address,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
}) {
if (widget.hub == null) {
widget.bloc.add(
EditHubAddRequested(
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
} else {
widget.bloc.add(
EditHubUpdateRequested(
id: widget.hub!.id,
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
}
},
),
return Scaffold(
appBar: UiAppBar(title: title, showBackButton: true),
body: Stack(
children: <Widget>[
SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: HubForm(
hub: hub,
costCenters: state.costCenters,
onCancel: () => Modular.to.pop(),
onSave:
({
required String name,
required String address,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
}) {
if (hub == null) {
BlocProvider.of<EditHubBloc>(context).add(
EditHubAddRequested(
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
} else {
BlocProvider.of<EditHubBloc>(context).add(
EditHubUpdateRequested(
id: hub!.id,
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
}
},
),
),
// Global loading overlay if saving
if (isSaving)
Container(
color: UiColors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()),
),
],
),
);
},
),
// Global loading overlay if saving
if (isSaving)
Container(
color: UiColors.black.withValues(alpha: 0.1),
child: const Center(child: CircularProgressIndicator()),
),
],
),
);
},
),
);
}

View File

@@ -10,28 +10,27 @@ import '../blocs/hub_details/hub_details_bloc.dart';
import '../blocs/hub_details/hub_details_event.dart';
import '../blocs/hub_details/hub_details_state.dart';
import '../widgets/hub_details/hub_details_bottom_actions.dart';
import '../widgets/hub_details/hub_details_header.dart';
import '../widgets/hub_details/hub_details_item.dart';
/// A read-only details page for a single [Hub].
///
/// Shows hub name, address, and NFC tag assignment.
class HubDetailsPage extends StatelessWidget {
const HubDetailsPage({required this.hub, required this.bloc, super.key});
const HubDetailsPage({required this.hub, super.key});
final Hub hub;
final HubDetailsBloc bloc;
@override
Widget build(BuildContext context) {
return BlocProvider<HubDetailsBloc>.value(
value: bloc,
return BlocProvider<HubDetailsBloc>(
create: (_) => Modular.get<HubDetailsBloc>(),
child: BlocListener<HubDetailsBloc, HubDetailsState>(
listener: (BuildContext context, HubDetailsState state) {
if (state.status == HubDetailsStatus.deleted) {
final String message = state.successKey == 'deleted'
? t.client_hubs.hub_details.deleted_success
: (state.successMessage ?? t.client_hubs.hub_details.deleted_success);
: (state.successMessage ??
t.client_hubs.hub_details.deleted_success);
UiSnackbar.show(
context,
message: message,
@@ -53,23 +52,22 @@ class HubDetailsPage extends StatelessWidget {
final bool isLoading = state.status == HubDetailsStatus.loading;
return Scaffold(
appBar: const UiAppBar(showBackButton: true),
appBar: UiAppBar(
title: hub.name,
subtitle: hub.address,
showBackButton: true,
),
bottomNavigationBar: HubDetailsBottomActions(
isLoading: isLoading,
onDelete: () => _confirmDeleteHub(context),
onEdit: () => _navigateToEditPage(context),
),
backgroundColor: UiColors.bgMenu,
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// ── Header ──────────────────────────────────────────
HubDetailsHeader(hub: hub),
const Divider(height: 1, thickness: 0.5),
Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
@@ -85,11 +83,16 @@ class HubDetailsPage extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
HubDetailsItem(
label: t.client_hubs.hub_details.cost_center_label,
label:
t.client_hubs.hub_details.cost_center_label,
value: hub.costCenter != null
? '${hub.costCenter!.name} (${hub.costCenter!.code})'
: t.client_hubs.hub_details.cost_center_none,
icon: UiIcons.bank, // Using bank icon for cost center
: t
.client_hubs
.hub_details
.cost_center_none,
icon: UiIcons
.bank, // Using bank icon for cost center
isHighlight: hub.costCenter != null,
),
],
@@ -140,7 +143,7 @@ class HubDetailsPage extends StatelessWidget {
);
if (confirm == true) {
bloc.add(HubDetailsDeleteRequested(hub.id));
Modular.get<HubDetailsBloc>().add(HubDetailsDeleteRequested(hub.id));
}
}
}

View File

@@ -0,0 +1,307 @@
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';
import 'edit_hub/edit_hub_field_label.dart';
/// A bottom sheet dialog for adding or editing a hub.
class HubForm extends StatefulWidget {
const HubForm({
required this.onSave,
required this.onCancel,
this.hub,
this.costCenters = const <CostCenter>[],
super.key,
});
final Hub? hub;
final List<CostCenter> costCenters;
final void Function({
required String name,
required String address,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
})
onSave;
final VoidCallback onCancel;
@override
State<HubForm> createState() => _HubFormState();
}
class _HubFormState extends State<HubForm> {
late final TextEditingController _nameController;
late final TextEditingController _addressController;
late final FocusNode _addressFocusNode;
String? _selectedCostCenterId;
Prediction? _selectedPrediction;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.hub?.name);
_addressController = TextEditingController(text: widget.hub?.address);
_addressFocusNode = FocusNode();
_selectedCostCenterId = widget.hub?.costCenter?.id;
}
@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 buttonText = isEditing
? t.client_hubs.edit_hub.save_button
: t.client_hubs.add_hub_dialog.create_button;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// ── Hub Name ────────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.name_label),
const SizedBox(height: UiConstants.space2),
TextFormField(
controller: _nameController,
textInputAction: TextInputAction.next,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return t.client_hubs.add_hub_dialog.name_required;
}
return null;
},
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.name_hint,
),
),
const SizedBox(height: UiConstants.space4),
// ── Cost Center ─────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.cost_center_label),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: _showCostCenterSelector,
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 16,
),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFD),
borderRadius: BorderRadius.circular(
UiConstants.radiusBase * 1.5,
),
border: Border.all(
color: _selectedCostCenterId != null
? UiColors.primary
: UiColors.primary.withValues(alpha: 0.1),
width: _selectedCostCenterId != null ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
_selectedCostCenterId != null
? _getCostCenterName(_selectedCostCenterId!)
: t.client_hubs.add_hub_dialog.cost_center_hint,
style: _selectedCostCenterId != null
? UiTypography.body1r.textPrimary
: UiTypography.body2r.textPlaceholder.copyWith(
color: UiColors.textSecondary.withValues(
alpha: 0.5,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(
Icons.keyboard_arrow_down,
color: UiColors.iconSecondary,
),
],
),
),
),
const SizedBox(height: UiConstants.space4),
// ── Address ─────────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.address_label),
const SizedBox(height: UiConstants.space2),
HubAddressAutocomplete(
controller: _addressController,
hintText: t.client_hubs.add_hub_dialog.address_hint,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.address_hint,
),
focusNode: _addressFocusNode,
onSelected: (Prediction prediction) {
_selectedPrediction = prediction;
},
),
const SizedBox(height: UiConstants.space8),
// ── Save Button ─────────────────────────────
Row(
children: <Widget>[
Expanded(
child: UiButton.primary(
onPressed: () {
if (_formKey.currentState!.validate()) {
if (_addressController.text.trim().isEmpty) {
UiSnackbar.show(
context,
message:
t.client_hubs.add_hub_dialog.address_required,
type: UiSnackbarType.error,
);
return;
}
widget.onSave(
name: _nameController.text.trim(),
address: _addressController.text.trim(),
costCenterId: _selectedCostCenterId,
placeId: _selectedPrediction?.placeId,
latitude: double.tryParse(
_selectedPrediction?.lat ?? '',
),
longitude: double.tryParse(
_selectedPrediction?.lng ?? '',
),
);
}
},
text: buttonText,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: UiButton.secondary(
onPressed: widget.onCancel,
text: t.common.cancel,
),
),
],
),
],
),
);
}
InputDecoration _buildInputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: UiTypography.body2r.textPlaceholder.copyWith(
color: UiColors.textSecondary.withValues(alpha: 0.5),
),
filled: true,
fillColor: const Color(0xFFF8FAFD),
contentPadding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: const BorderSide(color: UiColors.primary, width: 2),
),
errorStyle: UiTypography.footnote2r.textError,
);
}
String _getCostCenterName(String id) {
try {
return widget.costCenters.firstWhere((CostCenter cc) => cc.id == id).name;
} catch (_) {
return id;
}
}
Future<void> _showCostCenterSelector() async {
final CostCenter? selected = await showDialog<CostCenter>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
title: Text(
t.client_hubs.add_hub_dialog.cost_center_label,
style: UiTypography.headline3m.textPrimary,
),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: SizedBox(
width: double.maxFinite,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: widget.costCenters.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
t.client_hubs.add_hub_dialog.cost_centers_empty,
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: widget.costCenters.length,
itemBuilder: (BuildContext context, int index) {
final CostCenter cc = widget.costCenters[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
cc.name,
style: UiTypography.body1m.textPrimary,
),
subtitle: cc.code != null
? Text(
cc.code!,
style: UiTypography.body2r.textSecondary,
)
: null,
onTap: () => Navigator.of(context).pop(cc),
);
},
),
),
),
);
},
);
if (selected != null) {
setState(() {
_selectedCostCenterId = selected.id;
});
}
}
}

View File

@@ -1,350 +0,0 @@
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';
import 'edit_hub/edit_hub_field_label.dart';
/// A bottom sheet dialog for adding or editing a hub.
class HubFormDialog extends StatefulWidget {
/// Creates a [HubFormDialog].
const HubFormDialog({
required this.onSave,
required this.onCancel,
this.hub,
this.costCenters = const <CostCenter>[],
super.key,
});
/// The hub to edit. If null, a new hub is created.
final Hub? hub;
/// Available cost centers for selection.
final List<CostCenter> costCenters;
/// Callback when the "Save" button is pressed.
final void Function({
required String name,
required String address,
String? costCenterId,
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;
String? _selectedCostCenterId;
Prediction? _selectedPrediction;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.hub?.name);
_addressController = TextEditingController(text: widget.hub?.address);
_addressFocusNode = FocusNode();
_selectedCostCenterId = widget.hub?.costCenter?.id;
}
@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
? t.client_hubs.edit_hub.title
: t.client_hubs.add_hub_dialog.title;
final String buttonText = isEditing
? t.client_hubs.edit_hub.save_button
: t.client_hubs.add_hub_dialog.create_button;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
decoration: BoxDecoration(
color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 3),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.15),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(UiConstants.space6),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.textPrimary.copyWith(
fontSize: 20,
),
),
const SizedBox(height: UiConstants.space5),
// ── Hub Name ────────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.name_label),
const SizedBox(height: UiConstants.space2),
TextFormField(
controller: _nameController,
style: UiTypography.body1r.textPrimary,
textInputAction: TextInputAction.next,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
return t.client_hubs.add_hub_dialog.name_required;
}
return null;
},
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.name_hint,
),
),
const SizedBox(height: UiConstants.space4),
// ── Cost Center ─────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.cost_center_label),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: _showCostCenterSelector,
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 16,
),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFD),
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
border: Border.all(
color: _selectedCostCenterId != null
? UiColors.primary
: UiColors.primary.withValues(alpha: 0.1),
width: _selectedCostCenterId != null ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
_selectedCostCenterId != null
? _getCostCenterName(_selectedCostCenterId!)
: t.client_hubs.add_hub_dialog.cost_center_hint,
style: _selectedCostCenterId != null
? UiTypography.body1r.textPrimary
: UiTypography.body2r.textPlaceholder.copyWith(
color: UiColors.textSecondary.withValues(alpha: 0.5),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(
Icons.keyboard_arrow_down,
color: UiColors.iconSecondary,
),
],
),
),
),
const SizedBox(height: UiConstants.space4),
// ── Address ─────────────────────────────────
EditHubFieldLabel(t.client_hubs.add_hub_dialog.address_label),
const SizedBox(height: UiConstants.space2),
HubAddressAutocomplete(
controller: _addressController,
hintText: t.client_hubs.add_hub_dialog.address_hint,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.address_hint,
),
focusNode: _addressFocusNode,
onSelected: (Prediction prediction) {
_selectedPrediction = prediction;
},
),
const SizedBox(height: UiConstants.space8),
// ── Buttons ─────────────────────────────────
Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
style: OutlinedButton.styleFrom(
side: BorderSide(
color: UiColors.primary.withValues(alpha: 0.1),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase * 1.5,
),
),
),
onPressed: widget.onCancel,
text: t.common.cancel,
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.accent,
foregroundColor: UiColors.accentForeground,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase * 1.5,
),
),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
if (_addressController.text.trim().isEmpty) {
UiSnackbar.show(
context,
message: t.client_hubs.add_hub_dialog.address_required,
type: UiSnackbarType.error,
);
return;
}
widget.onSave(
name: _nameController.text.trim(),
address: _addressController.text.trim(),
costCenterId: _selectedCostCenterId,
placeId: _selectedPrediction?.placeId,
latitude: double.tryParse(
_selectedPrediction?.lat ?? '',
),
longitude: double.tryParse(
_selectedPrediction?.lng ?? '',
),
);
}
},
text: buttonText,
),
),
],
),
],
),
),
),
),
);
}
InputDecoration _buildInputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: UiTypography.body2r.textPlaceholder.copyWith(
color: UiColors.textSecondary.withValues(alpha: 0.5),
),
filled: true,
fillColor: const Color(0xFFF8FAFD),
contentPadding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: const BorderSide(color: UiColors.primary, width: 2),
),
errorStyle: UiTypography.footnote2r.textError,
);
}
String _getCostCenterName(String id) {
try {
return widget.costCenters.firstWhere((CostCenter cc) => cc.id == id).name;
} catch (_) {
return id;
}
}
Future<void> _showCostCenterSelector() async {
final CostCenter? selected = await showDialog<CostCenter>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
title: Text(
t.client_hubs.add_hub_dialog.cost_center_label,
style: UiTypography.headline3m.textPrimary,
),
contentPadding: const EdgeInsets.symmetric(vertical: 16),
content: SizedBox(
width: double.maxFinite,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: widget.costCenters.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(t.client_hubs.add_hub_dialog.cost_centers_empty),
)
: ListView.builder(
shrinkWrap: true,
itemCount: widget.costCenters.length,
itemBuilder: (BuildContext context, int index) {
final CostCenter cc = widget.costCenters[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(cc.name, style: UiTypography.body1m.textPrimary),
subtitle: cc.code != null ? Text(cc.code!, style: UiTypography.body2r.textSecondary) : null,
onTap: () => Navigator.of(context).pop(cc),
);
},
),
),
),
);
},
);
if (selected != null) {
setState(() {
_selectedCostCenterId = selected.id;
});
}
}
}