hub & manager issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
@@ -8,6 +9,7 @@ import 'src/domain/repositories/hub_repository_interface.dart';
|
||||
import 'src/domain/usecases/assign_nfc_tag_usecase.dart';
|
||||
import 'src/domain/usecases/create_hub_usecase.dart';
|
||||
import 'src/domain/usecases/delete_hub_usecase.dart';
|
||||
import 'src/domain/usecases/get_cost_centers_usecase.dart';
|
||||
import 'src/domain/usecases/get_hubs_usecase.dart';
|
||||
import 'src/domain/usecases/update_hub_usecase.dart';
|
||||
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
||||
@@ -32,6 +34,7 @@ class ClientHubsModule extends Module {
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(GetHubsUseCase.new);
|
||||
i.addLazySingleton(GetCostCentersUseCase.new);
|
||||
i.addLazySingleton(CreateHubUseCase.new);
|
||||
i.addLazySingleton(DeleteHubUseCase.new);
|
||||
i.addLazySingleton(AssignNfcTagUseCase.new);
|
||||
@@ -61,6 +64,18 @@ class ClientHubsModule extends Module {
|
||||
);
|
||||
r.child(
|
||||
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.editHub),
|
||||
transition: TransitionType.custom,
|
||||
customTransition: CustomTransition(
|
||||
opaque: false,
|
||||
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(
|
||||
|
||||
@@ -24,6 +24,17 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
return _connectorRepository.getHubs(businessId: businessId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CostCenter>> getCostCenters() async {
|
||||
// Mocking cost centers for now since the backend is not yet ready.
|
||||
return <CostCenter>[
|
||||
const CostCenter(id: 'cc-001', name: 'Kitchen', code: '1001'),
|
||||
const CostCenter(id: 'cc-002', name: 'Front Desk', code: '1002'),
|
||||
const CostCenter(id: 'cc-003', name: 'Waitstaff', code: '1003'),
|
||||
const CostCenter(id: 'cc-004', name: 'Management', code: '1004'),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Hub> createHub({
|
||||
required String name,
|
||||
@@ -36,7 +47,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
String? costCenter,
|
||||
String? costCenterId,
|
||||
}) async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.createHub(
|
||||
@@ -80,7 +91,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
String? costCenter,
|
||||
String? costCenterId,
|
||||
}) async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.updateHub(
|
||||
|
||||
@@ -19,7 +19,7 @@ class CreateHubArguments extends UseCaseArgument {
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
this.costCenter,
|
||||
this.costCenterId,
|
||||
});
|
||||
/// The name of the hub.
|
||||
final String name;
|
||||
@@ -37,7 +37,7 @@ class CreateHubArguments extends UseCaseArgument {
|
||||
final String? zipCode;
|
||||
|
||||
/// The cost center of the hub.
|
||||
final String? costCenter;
|
||||
final String? costCenterId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
@@ -51,6 +51,6 @@ class CreateHubArguments extends UseCaseArgument {
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
costCenter,
|
||||
costCenterId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ abstract interface class HubRepositoryInterface {
|
||||
/// Returns a list of [Hub] entities.
|
||||
Future<List<Hub>> getHubs();
|
||||
|
||||
/// Fetches the list of available cost centers for the current business.
|
||||
Future<List<CostCenter>> getCostCenters();
|
||||
|
||||
/// Creates a new hub.
|
||||
///
|
||||
/// Takes the [name] and [address] of the new hub.
|
||||
@@ -26,7 +29,7 @@ abstract interface class HubRepositoryInterface {
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
String? costCenter,
|
||||
String? costCenterId,
|
||||
});
|
||||
|
||||
/// Deletes a hub by its [id].
|
||||
@@ -52,6 +55,6 @@ abstract interface class HubRepositoryInterface {
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
String? costCenter,
|
||||
String? costCenterId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
|
||||
street: arguments.street,
|
||||
country: arguments.country,
|
||||
zipCode: arguments.zipCode,
|
||||
costCenterId: arguments.costCenterId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/hub_repository_interface.dart';
|
||||
|
||||
/// Usecase to fetch all available cost centers.
|
||||
class GetCostCentersUseCase {
|
||||
GetCostCentersUseCase({required HubRepositoryInterface repository})
|
||||
: _repository = repository;
|
||||
|
||||
final HubRepositoryInterface _repository;
|
||||
|
||||
Future<List<CostCenter>> call() async {
|
||||
return _repository.getCostCenters();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
this.costCenterId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
@@ -30,6 +31,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
final String? costCenterId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
@@ -44,6 +46,7 @@ class UpdateHubArguments extends UseCaseArgument {
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
costCenterId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,6 +70,7 @@ class UpdateHubUseCase implements UseCase<UpdateHubArguments, Hub> {
|
||||
street: params.street,
|
||||
country: params.country,
|
||||
zipCode: params.zipCode,
|
||||
costCenterId: params.costCenterId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../../domain/arguments/create_hub_arguments.dart';
|
||||
import '../../../domain/usecases/create_hub_usecase.dart';
|
||||
import '../../../domain/usecases/update_hub_usecase.dart';
|
||||
import '../../../domain/usecases/get_cost_centers_usecase.dart';
|
||||
import 'edit_hub_event.dart';
|
||||
import 'edit_hub_state.dart';
|
||||
|
||||
@@ -12,15 +14,36 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
|
||||
EditHubBloc({
|
||||
required CreateHubUseCase createHubUseCase,
|
||||
required UpdateHubUseCase updateHubUseCase,
|
||||
required GetCostCentersUseCase getCostCentersUseCase,
|
||||
}) : _createHubUseCase = createHubUseCase,
|
||||
_updateHubUseCase = updateHubUseCase,
|
||||
_getCostCentersUseCase = getCostCentersUseCase,
|
||||
super(const EditHubState()) {
|
||||
on<EditHubCostCentersLoadRequested>(_onCostCentersLoadRequested);
|
||||
on<EditHubAddRequested>(_onAddRequested);
|
||||
on<EditHubUpdateRequested>(_onUpdateRequested);
|
||||
}
|
||||
|
||||
final CreateHubUseCase _createHubUseCase;
|
||||
final UpdateHubUseCase _updateHubUseCase;
|
||||
final GetCostCentersUseCase _getCostCentersUseCase;
|
||||
|
||||
Future<void> _onCostCentersLoadRequested(
|
||||
EditHubCostCentersLoadRequested event,
|
||||
Emitter<EditHubState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final List<CostCenter> costCenters = await _getCostCentersUseCase.call();
|
||||
emit(state.copyWith(costCenters: costCenters));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: EditHubStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAddRequested(
|
||||
EditHubAddRequested event,
|
||||
@@ -43,6 +66,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
|
||||
street: event.street,
|
||||
country: event.country,
|
||||
zipCode: event.zipCode,
|
||||
costCenterId: event.costCenterId,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
@@ -79,6 +103,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
|
||||
street: event.street,
|
||||
country: event.country,
|
||||
zipCode: event.zipCode,
|
||||
costCenterId: event.costCenterId,
|
||||
),
|
||||
);
|
||||
emit(
|
||||
|
||||
@@ -8,6 +8,11 @@ abstract class EditHubEvent extends Equatable {
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Event triggered to load all available cost centers.
|
||||
class EditHubCostCentersLoadRequested extends EditHubEvent {
|
||||
const EditHubCostCentersLoadRequested();
|
||||
}
|
||||
|
||||
/// Event triggered to add a new hub.
|
||||
class EditHubAddRequested extends EditHubEvent {
|
||||
const EditHubAddRequested({
|
||||
@@ -21,6 +26,7 @@ class EditHubAddRequested extends EditHubEvent {
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
this.costCenterId,
|
||||
});
|
||||
|
||||
final String name;
|
||||
@@ -33,6 +39,7 @@ class EditHubAddRequested extends EditHubEvent {
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
final String? costCenterId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
@@ -46,6 +53,7 @@ class EditHubAddRequested extends EditHubEvent {
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
costCenterId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,6 +71,7 @@ class EditHubUpdateRequested extends EditHubEvent {
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
this.costCenterId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
@@ -76,6 +85,7 @@ class EditHubUpdateRequested extends EditHubEvent {
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
final String? costCenterId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
@@ -90,5 +100,6 @@ class EditHubUpdateRequested extends EditHubEvent {
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
costCenterId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Status of the edit hub operation.
|
||||
enum EditHubStatus {
|
||||
@@ -21,6 +22,7 @@ class EditHubState extends Equatable {
|
||||
this.status = EditHubStatus.initial,
|
||||
this.errorMessage,
|
||||
this.successMessage,
|
||||
this.costCenters = const <CostCenter>[],
|
||||
});
|
||||
|
||||
/// The status of the operation.
|
||||
@@ -32,19 +34,29 @@ class EditHubState extends Equatable {
|
||||
/// The success message if the operation succeeded.
|
||||
final String? successMessage;
|
||||
|
||||
/// Available cost centers for selection.
|
||||
final List<CostCenter> costCenters;
|
||||
|
||||
/// Create a copy of this state with the given fields replaced.
|
||||
EditHubState copyWith({
|
||||
EditHubStatus? status,
|
||||
String? errorMessage,
|
||||
String? successMessage,
|
||||
List<CostCenter>? costCenters,
|
||||
}) {
|
||||
return EditHubState(
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
successMessage: successMessage ?? this.successMessage,
|
||||
costCenters: costCenters ?? this.costCenters,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[status, errorMessage, successMessage];
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
errorMessage,
|
||||
successMessage,
|
||||
costCenters,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -57,20 +57,6 @@ class ClientHubsPage extends StatelessWidget {
|
||||
builder: (BuildContext context, ClientHubsState state) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
final bool? success = await Modular.to.toEditHub();
|
||||
if (success == true && context.mounted) {
|
||||
BlocProvider.of<ClientHubsBloc>(
|
||||
context,
|
||||
).add(const ClientHubsFetched());
|
||||
}
|
||||
},
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: const Icon(UiIcons.add),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
_buildAppBar(context),
|
||||
@@ -165,20 +151,35 @@ class ClientHubsPage extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_hubs.title,
|
||||
style: UiTypography.headline1m.white,
|
||||
),
|
||||
Text(
|
||||
t.client_hubs.subtitle,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.switchInactive,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_hubs.title,
|
||||
style: UiTypography.headline1m.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
t.client_hubs.subtitle,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.switchInactive,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiButton.primary(
|
||||
onPressed: () async {
|
||||
final bool? success = await Modular.to.toEditHub();
|
||||
if (success == true && context.mounted) {
|
||||
BlocProvider.of<ClientHubsBloc>(
|
||||
context,
|
||||
).add(const ClientHubsFetched());
|
||||
}
|
||||
},
|
||||
text: t.client_hubs.add_hub,
|
||||
leadingIcon: UiIcons.add,
|
||||
size: UiButtonSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
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:google_places_flutter/model/prediction.dart';
|
||||
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/edit_hub/edit_hub_form_section.dart';
|
||||
import '../widgets/hub_form_dialog.dart';
|
||||
|
||||
/// A dedicated full-screen page for adding or editing a hub.
|
||||
/// 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});
|
||||
|
||||
@@ -23,66 +21,11 @@ class EditHubPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EditHubPageState extends State<EditHubPage> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
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();
|
||||
|
||||
// Update header on change (if header is added back)
|
||||
_nameController.addListener(() => setState(() {}));
|
||||
_addressController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_addressController.dispose();
|
||||
_addressFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSave() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
if (_addressController.text.trim().isEmpty) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.client_hubs.add_hub_dialog.address_hint,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.hub == null) {
|
||||
widget.bloc.add(
|
||||
EditHubAddRequested(
|
||||
name: _nameController.text.trim(),
|
||||
address: _addressController.text.trim(),
|
||||
placeId: _selectedPrediction?.placeId,
|
||||
latitude: double.tryParse(_selectedPrediction?.lat ?? ''),
|
||||
longitude: double.tryParse(_selectedPrediction?.lng ?? ''),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget.bloc.add(
|
||||
EditHubUpdateRequested(
|
||||
id: widget.hub!.id,
|
||||
name: _nameController.text.trim(),
|
||||
address: _addressController.text.trim(),
|
||||
placeId: _selectedPrediction?.placeId,
|
||||
latitude: double.tryParse(_selectedPrediction?.lat ?? ''),
|
||||
longitude: double.tryParse(_selectedPrediction?.lng ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Load available cost centers
|
||||
widget.bloc.add(const EditHubCostCentersLoadRequested());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -101,7 +44,6 @@ class _EditHubPageState extends State<EditHubPage> {
|
||||
message: state.successMessage!,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
// Pop back to the previous screen.
|
||||
Modular.to.pop(true);
|
||||
}
|
||||
if (state.status == EditHubStatus.failure &&
|
||||
@@ -118,42 +60,59 @@ class _EditHubPageState extends State<EditHubPage> {
|
||||
final bool isSaving = state.status == EditHubStatus.loading;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
appBar: UiAppBar(
|
||||
title: widget.hub == null
|
||||
? t.client_hubs.add_hub_dialog.title
|
||||
: t.client_hubs.edit_hub.title,
|
||||
subtitle: widget.hub == null
|
||||
? t.client_hubs.add_hub_dialog.create_button
|
||||
: t.client_hubs.edit_hub.subtitle,
|
||||
onLeadingPressed: () => Modular.to.pop(),
|
||||
),
|
||||
backgroundColor: UiColors.bgOverlay,
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: EditHubFormSection(
|
||||
formKey: _formKey,
|
||||
nameController: _nameController,
|
||||
addressController: _addressController,
|
||||
addressFocusNode: _addressFocusNode,
|
||||
onAddressSelected: (Prediction prediction) {
|
||||
_selectedPrediction = prediction;
|
||||
},
|
||||
onSave: _onSave,
|
||||
isSaving: isSaving,
|
||||
isEdit: widget.hub != null,
|
||||
),
|
||||
),
|
||||
],
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ── Loading overlay ──────────────────────────────────────
|
||||
// Global loading overlay if saving
|
||||
if (isSaving)
|
||||
Container(
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
|
||||
@@ -80,6 +80,15 @@ class HubDetailsPage extends StatelessWidget {
|
||||
icon: UiIcons.nfc,
|
||||
isHighlight: hub.nfcTagId != null,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
HubDetailsItem(
|
||||
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
|
||||
isHighlight: hub.costCenter != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_places_flutter/model/prediction.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../hub_address_autocomplete.dart';
|
||||
import 'edit_hub_field_label.dart';
|
||||
@@ -15,6 +16,9 @@ class EditHubFormSection extends StatelessWidget {
|
||||
required this.addressFocusNode,
|
||||
required this.onAddressSelected,
|
||||
required this.onSave,
|
||||
this.costCenters = const <CostCenter>[],
|
||||
this.selectedCostCenterId,
|
||||
required this.onCostCenterChanged,
|
||||
this.isSaving = false,
|
||||
this.isEdit = false,
|
||||
super.key,
|
||||
@@ -26,6 +30,9 @@ class EditHubFormSection extends StatelessWidget {
|
||||
final FocusNode addressFocusNode;
|
||||
final ValueChanged<Prediction> onAddressSelected;
|
||||
final VoidCallback onSave;
|
||||
final List<CostCenter> costCenters;
|
||||
final String? selectedCostCenterId;
|
||||
final ValueChanged<String?> onCostCenterChanged;
|
||||
final bool isSaving;
|
||||
final bool isEdit;
|
||||
|
||||
@@ -62,6 +69,51 @@ class EditHubFormSection extends StatelessWidget {
|
||||
onSelected: onAddressSelected,
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
EditHubFieldLabel(t.client_hubs.edit_hub.cost_center_label),
|
||||
InkWell(
|
||||
onTap: () => _showCostCenterSelector(context),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: 14,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.input,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: selectedCostCenterId != null
|
||||
? UiColors.ring
|
||||
: UiColors.border,
|
||||
width: selectedCostCenterId != null ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedCostCenterId != null
|
||||
? _getCostCenterName(selectedCostCenterId!)
|
||||
: t.client_hubs.edit_hub.cost_center_hint,
|
||||
style: selectedCostCenterId != null
|
||||
? UiTypography.body1r.textPrimary
|
||||
: UiTypography.body2r.textPlaceholder,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// ── Save button ──────────────────────────────────
|
||||
@@ -102,4 +154,59 @@ class EditHubFormSection extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getCostCenterName(String id) {
|
||||
try {
|
||||
final CostCenter cc = costCenters.firstWhere((CostCenter item) => item.id == id);
|
||||
return cc.code != null ? '${cc.name} (${cc.code})' : cc.name;
|
||||
} catch (_) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCostCenterSelector(BuildContext context) 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.edit_hub.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: costCenters.isEmpty
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text('No cost centers available'),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: costCenters.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final CostCenter cc = 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) {
|
||||
onCostCenterChanged(selected.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class HubAddressAutocomplete extends StatelessWidget {
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
this.focusNode,
|
||||
this.decoration,
|
||||
this.onSelected,
|
||||
super.key,
|
||||
});
|
||||
@@ -18,6 +19,7 @@ class HubAddressAutocomplete extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
final FocusNode? focusNode;
|
||||
final InputDecoration? decoration;
|
||||
final void Function(Prediction prediction)? onSelected;
|
||||
|
||||
@override
|
||||
@@ -25,6 +27,7 @@ class HubAddressAutocomplete extends StatelessWidget {
|
||||
return GooglePlaceAutoCompleteTextField(
|
||||
textEditingController: controller,
|
||||
focusNode: focusNode,
|
||||
inputDecoration: decoration ?? const InputDecoration(),
|
||||
googleAPIKey: AppConfig.googleMapsApiKey,
|
||||
debounceTime: 500,
|
||||
countries: HubsConstants.supportedCountries,
|
||||
|
||||
@@ -5,25 +5,30 @@ 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 dialog for adding or editing a hub.
|
||||
/// 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(
|
||||
String name,
|
||||
String address, {
|
||||
final void Function({
|
||||
required String name,
|
||||
required String address,
|
||||
String? costCenterId,
|
||||
String? placeId,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
@@ -40,6 +45,7 @@ class _HubFormDialogState extends State<HubFormDialog> {
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _addressController;
|
||||
late final FocusNode _addressFocusNode;
|
||||
String? _selectedCostCenterId;
|
||||
Prediction? _selectedPrediction;
|
||||
|
||||
@override
|
||||
@@ -48,6 +54,7 @@ class _HubFormDialogState extends State<HubFormDialog> {
|
||||
_nameController = TextEditingController(text: widget.hub?.name);
|
||||
_addressController = TextEditingController(text: widget.hub?.address);
|
||||
_addressFocusNode = FocusNode();
|
||||
_selectedCostCenterId = widget.hub?.costCenter?.id;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -63,102 +70,193 @@ class _HubFormDialogState extends State<HubFormDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isEditing = widget.hub != null;
|
||||
final String title = isEditing
|
||||
? 'Edit Hub' // TODO: localize
|
||||
final String title = isEditing
|
||||
? t.client_hubs.edit_hub.title
|
||||
: t.client_hubs.add_hub_dialog.title;
|
||||
|
||||
|
||||
final String buttonText = isEditing
|
||||
? 'Save Changes' // TODO: localize
|
||||
? t.client_hubs.edit_hub.save_button
|
||||
: 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),
|
||||
],
|
||||
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),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
],
|
||||
),
|
||||
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),
|
||||
_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.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),
|
||||
_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.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,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
onPressed: widget.onCancel,
|
||||
text: t.common.cancel,
|
||||
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(
|
||||
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 ?? '',
|
||||
),
|
||||
),
|
||||
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;
|
||||
}
|
||||
},
|
||||
text: buttonText,
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -166,35 +264,87 @@ class _HubFormDialogState extends State<HubFormDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
hintStyle: UiTypography.body2r.textPlaceholder.copyWith(
|
||||
color: UiColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: UiColors.input,
|
||||
fillColor: const Color(0xFFF8FAFD),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: 14,
|
||||
vertical: 16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
|
||||
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
|
||||
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.ring, width: 2),
|
||||
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
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text('No cost centers available'),
|
||||
)
|
||||
: 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user