feat: refactor hub management to use dedicated pages for adding, editing, and viewing hub details.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
|
|
||||||
@@ -145,6 +146,22 @@ extension ClientNavigator on IModularNavigator {
|
|||||||
await pushNamed(ClientPaths.hubs);
|
await pushNamed(ClientPaths.hubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Navigates to the details of a specific hub.
|
||||||
|
Future<bool?> toHubDetails(Hub hub) {
|
||||||
|
return pushNamed<bool?>(
|
||||||
|
ClientPaths.hubDetails,
|
||||||
|
arguments: <String, dynamic>{'hub': hub},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the page to add a new hub or edit an existing one.
|
||||||
|
Future<bool?> toEditHub({Hub? hub}) async {
|
||||||
|
return pushNamed<bool?>(
|
||||||
|
ClientPaths.editHub,
|
||||||
|
arguments: <String, dynamic>{'hub': hub},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// ORDER CREATION
|
// ORDER CREATION
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -82,10 +82,12 @@ class ClientPaths {
|
|||||||
static const String billing = '/client-main/billing';
|
static const String billing = '/client-main/billing';
|
||||||
|
|
||||||
/// Completion review page - review shift completion records.
|
/// Completion review page - review shift completion records.
|
||||||
static const String completionReview = '/client-main/billing/completion-review';
|
static const String completionReview =
|
||||||
|
'/client-main/billing/completion-review';
|
||||||
|
|
||||||
/// Full list of invoices awaiting approval.
|
/// Full list of invoices awaiting approval.
|
||||||
static const String awaitingApproval = '/client-main/billing/awaiting-approval';
|
static const String awaitingApproval =
|
||||||
|
'/client-main/billing/awaiting-approval';
|
||||||
|
|
||||||
/// Invoice ready page - view status of approved invoices.
|
/// Invoice ready page - view status of approved invoices.
|
||||||
static const String invoiceReady = '/client-main/billing/invoice-ready';
|
static const String invoiceReady = '/client-main/billing/invoice-ready';
|
||||||
@@ -118,6 +120,12 @@ class ClientPaths {
|
|||||||
/// View and manage physical locations/hubs where staff are deployed.
|
/// View and manage physical locations/hubs where staff are deployed.
|
||||||
static const String hubs = '/client-hubs';
|
static const String hubs = '/client-hubs';
|
||||||
|
|
||||||
|
/// Specific hub details.
|
||||||
|
static const String hubDetails = '/client-hubs/details';
|
||||||
|
|
||||||
|
/// Page for adding or editing a hub.
|
||||||
|
static const String editHub = '/client-hubs/edit';
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// ORDER CREATION & MANAGEMENT
|
// ORDER CREATION & MANAGEMENT
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import 'src/domain/usecases/delete_hub_usecase.dart';
|
|||||||
import 'src/domain/usecases/get_hubs_usecase.dart';
|
import 'src/domain/usecases/get_hubs_usecase.dart';
|
||||||
import 'src/domain/usecases/update_hub_usecase.dart';
|
import 'src/domain/usecases/update_hub_usecase.dart';
|
||||||
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
||||||
|
import 'src/presentation/blocs/edit_hub/edit_hub_bloc.dart';
|
||||||
|
import 'src/presentation/blocs/hub_details/hub_details_bloc.dart';
|
||||||
import 'src/presentation/pages/client_hubs_page.dart';
|
import 'src/presentation/pages/client_hubs_page.dart';
|
||||||
|
import 'src/presentation/pages/edit_hub_page.dart';
|
||||||
|
import 'src/presentation/pages/hub_details_page.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
export 'src/presentation/pages/client_hubs_page.dart';
|
export 'src/presentation/pages/client_hubs_page.dart';
|
||||||
|
|
||||||
@@ -34,10 +39,35 @@ class ClientHubsModule extends Module {
|
|||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
||||||
|
i.add<EditHubBloc>(EditHubBloc.new);
|
||||||
|
i.add<HubDetailsBloc>(HubDetailsBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child(ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs), child: (_) => const ClientHubsPage());
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs),
|
||||||
|
child: (_) => const ClientHubsPage(),
|
||||||
|
);
|
||||||
|
r.child(
|
||||||
|
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>(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.editHub),
|
||||||
|
child: (_) {
|
||||||
|
final Map<String, dynamic> data = r.args.data as Map<String, dynamic>;
|
||||||
|
return EditHubPage(
|
||||||
|
hub: data['hub'] as Hub?,
|
||||||
|
bloc: Modular.get<EditHubBloc>(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,57 +3,38 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
|
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
|
||||||
import '../../domain/arguments/create_hub_arguments.dart';
|
|
||||||
import '../../domain/arguments/delete_hub_arguments.dart';
|
import '../../domain/arguments/delete_hub_arguments.dart';
|
||||||
import '../../domain/usecases/assign_nfc_tag_usecase.dart';
|
import '../../domain/usecases/assign_nfc_tag_usecase.dart';
|
||||||
import '../../domain/usecases/create_hub_usecase.dart';
|
|
||||||
import '../../domain/usecases/delete_hub_usecase.dart';
|
import '../../domain/usecases/delete_hub_usecase.dart';
|
||||||
import '../../domain/usecases/get_hubs_usecase.dart';
|
import '../../domain/usecases/get_hubs_usecase.dart';
|
||||||
import '../../domain/usecases/update_hub_usecase.dart';
|
|
||||||
import 'client_hubs_event.dart';
|
import 'client_hubs_event.dart';
|
||||||
import 'client_hubs_state.dart';
|
import 'client_hubs_state.dart';
|
||||||
|
|
||||||
/// BLoC responsible for managing the state of the Client Hubs feature.
|
/// BLoC responsible for managing the state of the Client Hubs feature.
|
||||||
///
|
///
|
||||||
/// It orchestrates the flow between the UI and the domain layer by invoking
|
/// It orchestrates the flow between the UI and the domain layer by invoking
|
||||||
/// specific use cases for fetching, creating, deleting, and assigning tags to hubs.
|
/// specific use cases for fetching, deleting, and assigning tags to hubs.
|
||||||
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||||
with BlocErrorHandler<ClientHubsState>
|
with BlocErrorHandler<ClientHubsState>
|
||||||
implements Disposable {
|
implements Disposable {
|
||||||
|
|
||||||
ClientHubsBloc({
|
ClientHubsBloc({
|
||||||
required GetHubsUseCase getHubsUseCase,
|
required GetHubsUseCase getHubsUseCase,
|
||||||
required CreateHubUseCase createHubUseCase,
|
|
||||||
required DeleteHubUseCase deleteHubUseCase,
|
required DeleteHubUseCase deleteHubUseCase,
|
||||||
required AssignNfcTagUseCase assignNfcTagUseCase,
|
required AssignNfcTagUseCase assignNfcTagUseCase,
|
||||||
required UpdateHubUseCase updateHubUseCase,
|
|
||||||
}) : _getHubsUseCase = getHubsUseCase,
|
}) : _getHubsUseCase = getHubsUseCase,
|
||||||
_createHubUseCase = createHubUseCase,
|
|
||||||
_deleteHubUseCase = deleteHubUseCase,
|
_deleteHubUseCase = deleteHubUseCase,
|
||||||
_assignNfcTagUseCase = assignNfcTagUseCase,
|
_assignNfcTagUseCase = assignNfcTagUseCase,
|
||||||
_updateHubUseCase = updateHubUseCase,
|
|
||||||
super(const ClientHubsState()) {
|
super(const ClientHubsState()) {
|
||||||
on<ClientHubsFetched>(_onFetched);
|
on<ClientHubsFetched>(_onFetched);
|
||||||
on<ClientHubsAddRequested>(_onAddRequested);
|
|
||||||
on<ClientHubsUpdateRequested>(_onUpdateRequested);
|
|
||||||
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
||||||
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
||||||
on<ClientHubsMessageCleared>(_onMessageCleared);
|
on<ClientHubsMessageCleared>(_onMessageCleared);
|
||||||
on<ClientHubsAddDialogToggled>(_onAddDialogToggled);
|
|
||||||
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
|
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
|
||||||
}
|
}
|
||||||
final GetHubsUseCase _getHubsUseCase;
|
final GetHubsUseCase _getHubsUseCase;
|
||||||
final CreateHubUseCase _createHubUseCase;
|
|
||||||
final DeleteHubUseCase _deleteHubUseCase;
|
final DeleteHubUseCase _deleteHubUseCase;
|
||||||
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
||||||
final UpdateHubUseCase _updateHubUseCase;
|
|
||||||
|
|
||||||
void _onAddDialogToggled(
|
|
||||||
ClientHubsAddDialogToggled event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) {
|
|
||||||
emit(state.copyWith(showAddHubDialog: event.visible));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onIdentifyDialogToggled(
|
void _onIdentifyDialogToggled(
|
||||||
ClientHubsIdentifyDialogToggled event,
|
ClientHubsIdentifyDialogToggled event,
|
||||||
@@ -75,7 +56,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
await handleError(
|
await handleError(
|
||||||
emit: emit.call,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
final List<Hub> hubs = await _getHubsUseCase.call();
|
||||||
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
||||||
},
|
},
|
||||||
onError: (String errorKey) => state.copyWith(
|
onError: (String errorKey) => state.copyWith(
|
||||||
@@ -85,86 +66,6 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAddRequested(
|
|
||||||
ClientHubsAddRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _createHubUseCase(
|
|
||||||
CreateHubArguments(
|
|
||||||
name: event.name,
|
|
||||||
address: event.address,
|
|
||||||
placeId: event.placeId,
|
|
||||||
latitude: event.latitude,
|
|
||||||
longitude: event.longitude,
|
|
||||||
city: event.city,
|
|
||||||
state: event.state,
|
|
||||||
street: event.street,
|
|
||||||
country: event.country,
|
|
||||||
zipCode: event.zipCode,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'Hub created successfully',
|
|
||||||
showAddHubDialog: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onUpdateRequested(
|
|
||||||
ClientHubsUpdateRequested event,
|
|
||||||
Emitter<ClientHubsState> emit,
|
|
||||||
) async {
|
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
|
||||||
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _updateHubUseCase(
|
|
||||||
UpdateHubArguments(
|
|
||||||
id: event.id,
|
|
||||||
name: event.name,
|
|
||||||
address: event.address,
|
|
||||||
placeId: event.placeId,
|
|
||||||
latitude: event.latitude,
|
|
||||||
longitude: event.longitude,
|
|
||||||
city: event.city,
|
|
||||||
state: event.state,
|
|
||||||
street: event.street,
|
|
||||||
country: event.country,
|
|
||||||
zipCode: event.zipCode,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionSuccess,
|
|
||||||
hubs: hubs,
|
|
||||||
successMessage: 'Hub updated successfully!',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (String errorKey) => state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: errorKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDeleteRequested(
|
Future<void> _onDeleteRequested(
|
||||||
ClientHubsDeleteRequested event,
|
ClientHubsDeleteRequested event,
|
||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
@@ -174,8 +75,8 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
await handleError(
|
await handleError(
|
||||||
emit: emit.call,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.hubId));
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
final List<Hub> hubs = await _getHubsUseCase.call();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ClientHubsStatus.actionSuccess,
|
status: ClientHubsStatus.actionSuccess,
|
||||||
@@ -200,10 +101,10 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
await handleError(
|
await handleError(
|
||||||
emit: emit.call,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
await _assignNfcTagUseCase(
|
await _assignNfcTagUseCase.call(
|
||||||
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
||||||
);
|
);
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
final List<Hub> hubs = await _getHubsUseCase.call();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ClientHubsStatus.actionSuccess,
|
status: ClientHubsStatus.actionSuccess,
|
||||||
|
|||||||
@@ -14,94 +14,8 @@ class ClientHubsFetched extends ClientHubsEvent {
|
|||||||
const ClientHubsFetched();
|
const ClientHubsFetched();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event triggered to add a new hub.
|
|
||||||
class ClientHubsAddRequested extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsAddRequested({
|
|
||||||
required this.name,
|
|
||||||
required this.address,
|
|
||||||
this.placeId,
|
|
||||||
this.latitude,
|
|
||||||
this.longitude,
|
|
||||||
this.city,
|
|
||||||
this.state,
|
|
||||||
this.street,
|
|
||||||
this.country,
|
|
||||||
this.zipCode,
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
placeId,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
street,
|
|
||||||
country,
|
|
||||||
zipCode,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to update an existing hub.
|
|
||||||
class ClientHubsUpdateRequested extends ClientHubsEvent {
|
|
||||||
const ClientHubsUpdateRequested({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required 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;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
placeId,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
city,
|
|
||||||
state,
|
|
||||||
street,
|
|
||||||
country,
|
|
||||||
zipCode,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to delete a hub.
|
/// Event triggered to delete a hub.
|
||||||
class ClientHubsDeleteRequested extends ClientHubsEvent {
|
class ClientHubsDeleteRequested extends ClientHubsEvent {
|
||||||
|
|
||||||
const ClientHubsDeleteRequested(this.hubId);
|
const ClientHubsDeleteRequested(this.hubId);
|
||||||
final String hubId;
|
final String hubId;
|
||||||
|
|
||||||
@@ -111,7 +25,6 @@ class ClientHubsDeleteRequested extends ClientHubsEvent {
|
|||||||
|
|
||||||
/// Event triggered to assign an NFC tag to a hub.
|
/// Event triggered to assign an NFC tag to a hub.
|
||||||
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
|
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
|
||||||
|
|
||||||
const ClientHubsNfcTagAssignRequested({
|
const ClientHubsNfcTagAssignRequested({
|
||||||
required this.hubId,
|
required this.hubId,
|
||||||
required this.nfcTagId,
|
required this.nfcTagId,
|
||||||
@@ -128,19 +41,8 @@ class ClientHubsMessageCleared extends ClientHubsEvent {
|
|||||||
const ClientHubsMessageCleared();
|
const ClientHubsMessageCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event triggered to toggle the visibility of the "Add Hub" dialog.
|
|
||||||
class ClientHubsAddDialogToggled extends ClientHubsEvent {
|
|
||||||
|
|
||||||
const ClientHubsAddDialogToggled({required this.visible});
|
|
||||||
final bool visible;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[visible];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered to toggle the visibility of the "Identify NFC" dialog.
|
/// Event triggered to toggle the visibility of the "Identify NFC" dialog.
|
||||||
class ClientHubsIdentifyDialogToggled extends ClientHubsEvent {
|
class ClientHubsIdentifyDialogToggled extends ClientHubsEvent {
|
||||||
|
|
||||||
const ClientHubsIdentifyDialogToggled({this.hub});
|
const ClientHubsIdentifyDialogToggled({this.hub});
|
||||||
final Hub? hub;
|
final Hub? hub;
|
||||||
|
|
||||||
|
|||||||
@@ -14,23 +14,19 @@ enum ClientHubsStatus {
|
|||||||
|
|
||||||
/// State class for the ClientHubs BLoC.
|
/// State class for the ClientHubs BLoC.
|
||||||
class ClientHubsState extends Equatable {
|
class ClientHubsState extends Equatable {
|
||||||
|
|
||||||
const ClientHubsState({
|
const ClientHubsState({
|
||||||
this.status = ClientHubsStatus.initial,
|
this.status = ClientHubsStatus.initial,
|
||||||
this.hubs = const <Hub>[],
|
this.hubs = const <Hub>[],
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.successMessage,
|
this.successMessage,
|
||||||
this.showAddHubDialog = false,
|
|
||||||
this.hubToIdentify,
|
this.hubToIdentify,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ClientHubsStatus status;
|
final ClientHubsStatus status;
|
||||||
final List<Hub> hubs;
|
final List<Hub> hubs;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String? successMessage;
|
final String? successMessage;
|
||||||
|
|
||||||
/// Whether the "Add Hub" dialog should be visible.
|
|
||||||
final bool showAddHubDialog;
|
|
||||||
|
|
||||||
/// The hub currently being identified/assigned an NFC tag.
|
/// The hub currently being identified/assigned an NFC tag.
|
||||||
/// If null, the identification dialog is closed.
|
/// If null, the identification dialog is closed.
|
||||||
final Hub? hubToIdentify;
|
final Hub? hubToIdentify;
|
||||||
@@ -40,7 +36,6 @@ class ClientHubsState extends Equatable {
|
|||||||
List<Hub>? hubs,
|
List<Hub>? hubs,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
String? successMessage,
|
String? successMessage,
|
||||||
bool? showAddHubDialog,
|
|
||||||
Hub? hubToIdentify,
|
Hub? hubToIdentify,
|
||||||
bool clearHubToIdentify = false,
|
bool clearHubToIdentify = false,
|
||||||
bool clearErrorMessage = false,
|
bool clearErrorMessage = false,
|
||||||
@@ -55,7 +50,6 @@ class ClientHubsState extends Equatable {
|
|||||||
successMessage: clearSuccessMessage
|
successMessage: clearSuccessMessage
|
||||||
? null
|
? null
|
||||||
: (successMessage ?? this.successMessage),
|
: (successMessage ?? this.successMessage),
|
||||||
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
|
|
||||||
hubToIdentify: clearHubToIdentify
|
hubToIdentify: clearHubToIdentify
|
||||||
? null
|
? null
|
||||||
: (hubToIdentify ?? this.hubToIdentify),
|
: (hubToIdentify ?? this.hubToIdentify),
|
||||||
@@ -68,7 +62,6 @@ class ClientHubsState extends Equatable {
|
|||||||
hubs,
|
hubs,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
successMessage,
|
successMessage,
|
||||||
showAddHubDialog,
|
|
||||||
hubToIdentify,
|
hubToIdentify,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../../../domain/arguments/create_hub_arguments.dart';
|
||||||
|
import '../../../domain/usecases/create_hub_usecase.dart';
|
||||||
|
import '../../../domain/usecases/update_hub_usecase.dart';
|
||||||
|
import 'edit_hub_event.dart';
|
||||||
|
import 'edit_hub_state.dart';
|
||||||
|
|
||||||
|
/// Bloc for creating and updating hubs.
|
||||||
|
class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
|
||||||
|
with BlocErrorHandler<EditHubState> {
|
||||||
|
EditHubBloc({
|
||||||
|
required CreateHubUseCase createHubUseCase,
|
||||||
|
required UpdateHubUseCase updateHubUseCase,
|
||||||
|
}) : _createHubUseCase = createHubUseCase,
|
||||||
|
_updateHubUseCase = updateHubUseCase,
|
||||||
|
super(const EditHubState()) {
|
||||||
|
on<EditHubAddRequested>(_onAddRequested);
|
||||||
|
on<EditHubUpdateRequested>(_onUpdateRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CreateHubUseCase _createHubUseCase;
|
||||||
|
final UpdateHubUseCase _updateHubUseCase;
|
||||||
|
|
||||||
|
Future<void> _onAddRequested(
|
||||||
|
EditHubAddRequested event,
|
||||||
|
Emitter<EditHubState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EditHubStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
await _createHubUseCase.call(
|
||||||
|
CreateHubArguments(
|
||||||
|
name: event.name,
|
||||||
|
address: event.address,
|
||||||
|
placeId: event.placeId,
|
||||||
|
latitude: event.latitude,
|
||||||
|
longitude: event.longitude,
|
||||||
|
city: event.city,
|
||||||
|
state: event.state,
|
||||||
|
street: event.street,
|
||||||
|
country: event.country,
|
||||||
|
zipCode: event.zipCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EditHubStatus.success,
|
||||||
|
successMessage: 'Hub created successfully',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) =>
|
||||||
|
state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpdateRequested(
|
||||||
|
EditHubUpdateRequested event,
|
||||||
|
Emitter<EditHubState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: EditHubStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
await _updateHubUseCase.call(
|
||||||
|
UpdateHubArguments(
|
||||||
|
id: event.id,
|
||||||
|
name: event.name,
|
||||||
|
address: event.address,
|
||||||
|
placeId: event.placeId,
|
||||||
|
latitude: event.latitude,
|
||||||
|
longitude: event.longitude,
|
||||||
|
city: event.city,
|
||||||
|
state: event.state,
|
||||||
|
street: event.street,
|
||||||
|
country: event.country,
|
||||||
|
zipCode: event.zipCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: EditHubStatus.success,
|
||||||
|
successMessage: 'Hub updated successfully',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) =>
|
||||||
|
state.copyWith(status: EditHubStatus.failure, errorMessage: errorKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Base class for all edit hub events.
|
||||||
|
abstract class EditHubEvent extends Equatable {
|
||||||
|
const EditHubEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to add a new hub.
|
||||||
|
class EditHubAddRequested extends EditHubEvent {
|
||||||
|
const EditHubAddRequested({
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to update an existing hub.
|
||||||
|
class EditHubUpdateRequested extends EditHubEvent {
|
||||||
|
const EditHubUpdateRequested({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required 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;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Status of the edit hub operation.
|
||||||
|
enum EditHubStatus {
|
||||||
|
/// Initial state.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// Operation in progress.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Operation succeeded.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// Operation failed.
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the edit hub operation.
|
||||||
|
class EditHubState extends Equatable {
|
||||||
|
const EditHubState({
|
||||||
|
this.status = EditHubStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.successMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The status of the operation.
|
||||||
|
final EditHubStatus status;
|
||||||
|
|
||||||
|
/// The error message if the operation failed.
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// The success message if the operation succeeded.
|
||||||
|
final String? successMessage;
|
||||||
|
|
||||||
|
/// Create a copy of this state with the given fields replaced.
|
||||||
|
EditHubState copyWith({
|
||||||
|
EditHubStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
String? successMessage,
|
||||||
|
}) {
|
||||||
|
return EditHubState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
successMessage: successMessage ?? this.successMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[status, errorMessage, successMessage];
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../../../domain/arguments/assign_nfc_tag_arguments.dart';
|
||||||
|
import '../../../domain/arguments/delete_hub_arguments.dart';
|
||||||
|
import '../../../domain/usecases/assign_nfc_tag_usecase.dart';
|
||||||
|
import '../../../domain/usecases/delete_hub_usecase.dart';
|
||||||
|
import 'hub_details_event.dart';
|
||||||
|
import 'hub_details_state.dart';
|
||||||
|
|
||||||
|
/// Bloc for managing hub details and operations like delete and NFC assignment.
|
||||||
|
class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
|
||||||
|
with BlocErrorHandler<HubDetailsState> {
|
||||||
|
HubDetailsBloc({
|
||||||
|
required DeleteHubUseCase deleteHubUseCase,
|
||||||
|
required AssignNfcTagUseCase assignNfcTagUseCase,
|
||||||
|
}) : _deleteHubUseCase = deleteHubUseCase,
|
||||||
|
_assignNfcTagUseCase = assignNfcTagUseCase,
|
||||||
|
super(const HubDetailsState()) {
|
||||||
|
on<HubDetailsDeleteRequested>(_onDeleteRequested);
|
||||||
|
on<HubDetailsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
final DeleteHubUseCase _deleteHubUseCase;
|
||||||
|
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
||||||
|
|
||||||
|
Future<void> _onDeleteRequested(
|
||||||
|
HubDetailsDeleteRequested event,
|
||||||
|
Emitter<HubDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: HubDetailsStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id));
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: HubDetailsStatus.deleted,
|
||||||
|
successMessage: 'Hub deleted successfully',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: HubDetailsStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNfcTagAssignRequested(
|
||||||
|
HubDetailsNfcTagAssignRequested event,
|
||||||
|
Emitter<HubDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: HubDetailsStatus.loading));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
await _assignNfcTagUseCase.call(
|
||||||
|
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: HubDetailsStatus.success,
|
||||||
|
successMessage: 'NFC tag assigned successfully',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: HubDetailsStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Base class for all hub details events.
|
||||||
|
abstract class HubDetailsEvent extends Equatable {
|
||||||
|
const HubDetailsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to delete a hub.
|
||||||
|
class HubDetailsDeleteRequested extends HubDetailsEvent {
|
||||||
|
const HubDetailsDeleteRequested(this.id);
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered to assign an NFC tag to a hub.
|
||||||
|
class HubDetailsNfcTagAssignRequested extends HubDetailsEvent {
|
||||||
|
const HubDetailsNfcTagAssignRequested({
|
||||||
|
required this.hubId,
|
||||||
|
required this.nfcTagId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String hubId;
|
||||||
|
final String nfcTagId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[hubId, nfcTagId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Status of the hub details operation.
|
||||||
|
enum HubDetailsStatus {
|
||||||
|
/// Initial state.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// Operation in progress.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Operation succeeded.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// Operation failed.
|
||||||
|
failure,
|
||||||
|
|
||||||
|
/// Hub was deleted.
|
||||||
|
deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the hub details operation.
|
||||||
|
class HubDetailsState extends Equatable {
|
||||||
|
const HubDetailsState({
|
||||||
|
this.status = HubDetailsStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.successMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The status of the operation.
|
||||||
|
final HubDetailsStatus status;
|
||||||
|
|
||||||
|
/// The error message if the operation failed.
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// The success message if the operation succeeded.
|
||||||
|
final String? successMessage;
|
||||||
|
|
||||||
|
/// Create a copy of this state with the given fields replaced.
|
||||||
|
HubDetailsState copyWith({
|
||||||
|
HubDetailsStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
String? successMessage,
|
||||||
|
}) {
|
||||||
|
return HubDetailsState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
successMessage: successMessage ?? this.successMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[status, errorMessage, successMessage];
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/client_hubs_bloc.dart';
|
||||||
import '../blocs/client_hubs_event.dart';
|
import '../blocs/client_hubs_event.dart';
|
||||||
import '../blocs/client_hubs_state.dart';
|
import '../blocs/client_hubs_state.dart';
|
||||||
import '../widgets/add_hub_dialog.dart';
|
|
||||||
import '../widgets/hub_card.dart';
|
import '../widgets/hub_card.dart';
|
||||||
import '../widgets/hub_empty_state.dart';
|
import '../widgets/hub_empty_state.dart';
|
||||||
import '../widgets/hub_info_card.dart';
|
import '../widgets/hub_info_card.dart';
|
||||||
@@ -43,7 +43,8 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
).add(const ClientHubsMessageCleared());
|
).add(const ClientHubsMessageCleared());
|
||||||
}
|
}
|
||||||
if (state.successMessage != null && state.successMessage!.isNotEmpty) {
|
if (state.successMessage != null &&
|
||||||
|
state.successMessage!.isNotEmpty) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: state.successMessage!,
|
message: state.successMessage!,
|
||||||
@@ -58,9 +59,14 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgMenu,
|
backgroundColor: UiColors.bgMenu,
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => BlocProvider.of<ClientHubsBloc>(
|
onPressed: () async {
|
||||||
|
final bool? success = await Modular.to.toEditHub();
|
||||||
|
if (success == true && context.mounted) {
|
||||||
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
context,
|
context,
|
||||||
).add(const ClientHubsAddDialogToggled(visible: true)),
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
@@ -82,27 +88,37 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
const Center(child: CircularProgressIndicator())
|
const Center(child: CircularProgressIndicator())
|
||||||
else if (state.hubs.isEmpty)
|
else if (state.hubs.isEmpty)
|
||||||
HubEmptyState(
|
HubEmptyState(
|
||||||
onAddPressed: () =>
|
onAddPressed: () async {
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
final bool? success = await Modular.to
|
||||||
const ClientHubsAddDialogToggled(
|
.toEditHub();
|
||||||
visible: true,
|
if (success == true && context.mounted) {
|
||||||
),
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
),
|
context,
|
||||||
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
else ...<Widget>[
|
else ...<Widget>[
|
||||||
...state.hubs.map(
|
...state.hubs.map(
|
||||||
(Hub hub) => HubCard(
|
(Hub hub) => HubCard(
|
||||||
hub: hub,
|
hub: hub,
|
||||||
|
onTap: () async {
|
||||||
|
final bool? success = await Modular.to
|
||||||
|
.toHubDetails(hub);
|
||||||
|
if (success == true && context.mounted) {
|
||||||
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
|
context,
|
||||||
|
).add(const ClientHubsFetched());
|
||||||
|
}
|
||||||
|
},
|
||||||
onNfcPressed: () =>
|
onNfcPressed: () =>
|
||||||
BlocProvider.of<ClientHubsBloc>(
|
BlocProvider.of<ClientHubsBloc>(
|
||||||
context,
|
context,
|
||||||
).add(
|
).add(
|
||||||
ClientHubsIdentifyDialogToggled(hub: hub),
|
ClientHubsIdentifyDialogToggled(hub: hub),
|
||||||
),
|
),
|
||||||
onDeletePressed: () => _confirmDeleteHub(
|
onDeletePressed: () =>
|
||||||
context,
|
_confirmDeleteHub(context, hub),
|
||||||
hub,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -113,29 +129,7 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (state.showAddHubDialog)
|
|
||||||
AddHubDialog(
|
|
||||||
onCreate: (
|
|
||||||
String name,
|
|
||||||
String address, {
|
|
||||||
String? placeId,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
}) {
|
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
|
||||||
ClientHubsAddRequested(
|
|
||||||
name: name,
|
|
||||||
address: address,
|
|
||||||
placeId: placeId,
|
|
||||||
latitude: latitude,
|
|
||||||
longitude: longitude,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onCancel: () => BlocProvider.of<ClientHubsBloc>(
|
|
||||||
context,
|
|
||||||
).add(const ClientHubsAddDialogToggled(visible: false)),
|
|
||||||
),
|
|
||||||
if (state.hubToIdentify != null)
|
if (state.hubToIdentify != null)
|
||||||
IdentifyNfcDialog(
|
IdentifyNfcDialog(
|
||||||
hub: state.hubToIdentify!,
|
hub: state.hubToIdentify!,
|
||||||
|
|||||||
@@ -2,28 +2,21 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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:google_places_flutter/model/prediction.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/edit_hub/edit_hub_bloc.dart';
|
||||||
import '../blocs/client_hubs_event.dart';
|
import '../blocs/edit_hub/edit_hub_event.dart';
|
||||||
import '../blocs/client_hubs_state.dart';
|
import '../blocs/edit_hub/edit_hub_state.dart';
|
||||||
import '../widgets/hub_address_autocomplete.dart';
|
import '../widgets/hub_address_autocomplete.dart';
|
||||||
|
|
||||||
/// A dedicated full-screen page for editing an existing hub.
|
/// A dedicated full-screen page for adding or editing a hub.
|
||||||
///
|
|
||||||
/// Takes the parent [ClientHubsBloc] via [BlocProvider.value] so the
|
|
||||||
/// updated hub list is reflected on the hubs list page when the user
|
|
||||||
/// saves and navigates back.
|
|
||||||
class EditHubPage extends StatefulWidget {
|
class EditHubPage extends StatefulWidget {
|
||||||
const EditHubPage({
|
const EditHubPage({this.hub, required this.bloc, super.key});
|
||||||
required this.hub,
|
|
||||||
required this.bloc,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Hub hub;
|
final Hub? hub;
|
||||||
final ClientHubsBloc bloc;
|
final EditHubBloc bloc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EditHubPage> createState() => _EditHubPageState();
|
State<EditHubPage> createState() => _EditHubPageState();
|
||||||
@@ -39,8 +32,8 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController = TextEditingController(text: widget.hub.name);
|
_nameController = TextEditingController(text: widget.hub?.name);
|
||||||
_addressController = TextEditingController(text: widget.hub.address);
|
_addressController = TextEditingController(text: widget.hub?.address);
|
||||||
_addressFocusNode = FocusNode();
|
_addressFocusNode = FocusNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +57,20 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadContext(context).read<ClientHubsBloc>().add(
|
if (widget.hub == null) {
|
||||||
ClientHubsUpdateRequested(
|
widget.bloc.add(
|
||||||
id: widget.hub.id,
|
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(),
|
name: _nameController.text.trim(),
|
||||||
address: _addressController.text.trim(),
|
address: _addressController.text.trim(),
|
||||||
placeId: _selectedPrediction?.placeId,
|
placeId: _selectedPrediction?.placeId,
|
||||||
@@ -75,26 +79,28 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ClientHubsBloc>.value(
|
return BlocProvider<EditHubBloc>.value(
|
||||||
value: widget.bloc,
|
value: widget.bloc,
|
||||||
child: BlocListener<ClientHubsBloc, ClientHubsState>(
|
child: BlocListener<EditHubBloc, EditHubState>(
|
||||||
listenWhen: (ClientHubsState prev, ClientHubsState curr) =>
|
listenWhen: (EditHubState prev, EditHubState curr) =>
|
||||||
prev.status != curr.status || prev.successMessage != curr.successMessage,
|
prev.status != curr.status ||
|
||||||
listener: (BuildContext context, ClientHubsState state) {
|
prev.successMessage != curr.successMessage,
|
||||||
if (state.status == ClientHubsStatus.actionSuccess &&
|
listener: (BuildContext context, EditHubState state) {
|
||||||
|
if (state.status == EditHubStatus.success &&
|
||||||
state.successMessage != null) {
|
state.successMessage != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: state.successMessage!,
|
message: state.successMessage!,
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
// Pop back to details page with updated hub
|
// Pop back to the previous screen.
|
||||||
Navigator.of(context).pop(true);
|
Modular.to.pop(true);
|
||||||
}
|
}
|
||||||
if (state.status == ClientHubsStatus.actionFailure &&
|
if (state.status == EditHubStatus.failure &&
|
||||||
state.errorMessage != null) {
|
state.errorMessage != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
@@ -103,10 +109,9 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<ClientHubsBloc, ClientHubsState>(
|
child: BlocBuilder<EditHubBloc, EditHubState>(
|
||||||
builder: (BuildContext context, ClientHubsState state) {
|
builder: (BuildContext context, EditHubState state) {
|
||||||
final bool isSaving =
|
final bool isSaving = state.status == EditHubStatus.loading;
|
||||||
state.status == ClientHubsStatus.actionInProgress;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgMenu,
|
backgroundColor: UiColors.bgMenu,
|
||||||
@@ -114,17 +119,21 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
backgroundColor: UiColors.foreground,
|
backgroundColor: UiColors.foreground,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Modular.to.pop(),
|
||||||
),
|
),
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
t.client_hubs.edit_hub.title,
|
widget.hub == null
|
||||||
|
? t.client_hubs.add_hub_dialog.title
|
||||||
|
: t.client_hubs.edit_hub.title,
|
||||||
style: UiTypography.headline3m.white,
|
style: UiTypography.headline3m.white,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
t.client_hubs.edit_hub.subtitle,
|
widget.hub == null
|
||||||
|
? t.client_hubs.add_hub_dialog.create_button
|
||||||
|
: t.client_hubs.edit_hub.subtitle,
|
||||||
style: UiTypography.footnote1r.copyWith(
|
style: UiTypography.footnote1r.copyWith(
|
||||||
color: UiColors.white.withValues(alpha: 0.7),
|
color: UiColors.white.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
@@ -176,7 +185,9 @@ class _EditHubPageState extends State<EditHubPage> {
|
|||||||
// ── Save button ──────────────────────────────────
|
// ── Save button ──────────────────────────────────
|
||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
onPressed: isSaving ? null : _onSave,
|
onPressed: isSaving ? null : _onSave,
|
||||||
text: t.client_hubs.edit_hub.save_button,
|
text: widget.hub == null
|
||||||
|
? t.client_hubs.add_hub_dialog.create_button
|
||||||
|
: t.client_hubs.edit_hub.save_button,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|||||||
@@ -1,37 +1,64 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/hub_details/hub_details_bloc.dart';
|
||||||
import 'edit_hub_page.dart';
|
import '../blocs/hub_details/hub_details_event.dart';
|
||||||
|
import '../blocs/hub_details/hub_details_state.dart';
|
||||||
|
|
||||||
/// A read-only details page for a single [Hub].
|
/// A read-only details page for a single [Hub].
|
||||||
///
|
///
|
||||||
/// Shows hub name, address, and NFC tag assignment.
|
/// Shows hub name, address, and NFC tag assignment.
|
||||||
/// Tapping the edit button navigates to [EditHubPage] (a dedicated page,
|
|
||||||
/// not a dialog), satisfying the "separate edit hub page" acceptance criterion.
|
|
||||||
class HubDetailsPage extends StatelessWidget {
|
class HubDetailsPage extends StatelessWidget {
|
||||||
const HubDetailsPage({
|
const HubDetailsPage({required this.hub, required this.bloc, super.key});
|
||||||
required this.hub,
|
|
||||||
required this.bloc,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Hub hub;
|
final Hub hub;
|
||||||
final ClientHubsBloc bloc;
|
final HubDetailsBloc bloc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocProvider<HubDetailsBloc>.value(
|
||||||
|
value: bloc,
|
||||||
|
child: BlocListener<HubDetailsBloc, HubDetailsState>(
|
||||||
|
listener: (BuildContext context, HubDetailsState state) {
|
||||||
|
if (state.status == HubDetailsStatus.deleted) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: state.successMessage ?? 'Hub deleted successfully',
|
||||||
|
type: UiSnackbarType.success,
|
||||||
|
);
|
||||||
|
Modular.to.pop(true); // Return true to indicate change
|
||||||
|
}
|
||||||
|
if (state.status == HubDetailsStatus.failure &&
|
||||||
|
state.errorMessage != null) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: state.errorMessage!,
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(hub.name),
|
title: Text(hub.name),
|
||||||
backgroundColor: UiColors.foreground,
|
backgroundColor: UiColors.foreground,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Modular.to.pop(),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _confirmDeleteHub(context),
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.delete,
|
||||||
|
color: UiColors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => _navigateToEditPage(context),
|
onPressed: () => _navigateToEditPage(context),
|
||||||
icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16),
|
icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16),
|
||||||
@@ -62,13 +89,17 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
label: t.client_hubs.hub_details.nfc_label,
|
label: t.client_hubs.hub_details.nfc_label,
|
||||||
value: hub.nfcTagId ?? t.client_hubs.hub_details.nfc_not_assigned,
|
value:
|
||||||
|
hub.nfcTagId ??
|
||||||
|
t.client_hubs.hub_details.nfc_not_assigned,
|
||||||
icon: UiIcons.nfc,
|
icon: UiIcons.nfc,
|
||||||
isHighlight: hub.nfcTagId != null,
|
isHighlight: hub.nfcTagId != null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +127,9 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField,
|
color: isHighlight
|
||||||
|
? UiColors.tagInProgress
|
||||||
|
: UiColors.bgInputField,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -122,16 +155,37 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _navigateToEditPage(BuildContext context) async {
|
Future<void> _navigateToEditPage(BuildContext context) async {
|
||||||
// Navigate to the dedicated edit page and await result.
|
// We still need to pass a Bloc for the edit page, but it's handled by Modular.
|
||||||
// If the page returns `true` (save succeeded), pop the details page too so
|
// However, the Navigator extension expect a Bloc.
|
||||||
// the user sees the refreshed hub list (the BLoC already holds updated data).
|
// I'll update the Navigator extension to NOT require a Bloc since it's in Modular.
|
||||||
final bool? saved = await Navigator.of(context).push<bool>(
|
final bool? saved = await Modular.to.toEditHub(hub: hub);
|
||||||
MaterialPageRoute<bool>(
|
if (saved == true && context.mounted) {
|
||||||
builder: (_) => EditHubPage(hub: hub, bloc: bloc),
|
Modular.to.pop(true); // Return true to indicate change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmDeleteHub(BuildContext context) async {
|
||||||
|
final bool? confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => AlertDialog(
|
||||||
|
title: Text(t.client_hubs.delete_dialog.title),
|
||||||
|
content: Text(t.client_hubs.delete_dialog.message(hubName: hub.name)),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(t.client_hubs.delete_dialog.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||||
|
child: Text(t.client_hubs.delete_dialog.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (saved == true && context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
if (confirm == true) {
|
||||||
|
bloc.add(HubDetailsDeleteRequested(hub.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +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 'hub_address_autocomplete.dart';
|
|
||||||
|
|
||||||
/// A dialog for adding a new hub.
|
|
||||||
class AddHubDialog extends StatefulWidget {
|
|
||||||
|
|
||||||
/// Creates an [AddHubDialog].
|
|
||||||
const AddHubDialog({
|
|
||||||
required this.onCreate,
|
|
||||||
required this.onCancel,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
/// Callback when the "Create Hub" button is pressed.
|
|
||||||
final void Function(
|
|
||||||
String name,
|
|
||||||
String address, {
|
|
||||||
String? placeId,
|
|
||||||
double? latitude,
|
|
||||||
double? longitude,
|
|
||||||
}) onCreate;
|
|
||||||
|
|
||||||
/// Callback when the dialog is cancelled.
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AddHubDialog> createState() => _AddHubDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddHubDialogState extends State<AddHubDialog> {
|
|
||||||
late final TextEditingController _nameController;
|
|
||||||
late final TextEditingController _addressController;
|
|
||||||
late final FocusNode _addressFocusNode;
|
|
||||||
Prediction? _selectedPrediction;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_nameController = TextEditingController();
|
|
||||||
_addressController = TextEditingController();
|
|
||||||
_addressFocusNode = FocusNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
_addressController.dispose();
|
|
||||||
_addressFocusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
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(
|
|
||||||
t.client_hubs.add_hub_dialog.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),
|
|
||||||
// Assuming HubAddressAutocomplete is a custom widget wrapper.
|
|
||||||
// If it doesn't expose a validator, we might need to modify it or manually check _addressController.
|
|
||||||
// For now, let's just make sure we validate name. Address is tricky if it's a wrapper.
|
|
||||||
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()) {
|
|
||||||
// Manually check address if needed, or assume manual entry is ok.
|
|
||||||
if (_addressController.text.trim().isEmpty) {
|
|
||||||
// Show manual error or scaffold
|
|
||||||
UiSnackbar.show(context, message: 'Address is required', type: UiSnackbarType.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.onCreate(
|
|
||||||
_nameController.text,
|
|
||||||
_addressController.text,
|
|
||||||
placeId: _selectedPrediction?.placeId,
|
|
||||||
latitude: double.tryParse(
|
|
||||||
_selectedPrediction?.lat ?? '',
|
|
||||||
),
|
|
||||||
longitude: double.tryParse(
|
|
||||||
_selectedPrediction?.lng ?? '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text: t.client_hubs.add_hub_dialog.create_button,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,15 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
|
|
||||||
/// A card displaying information about a single hub.
|
/// A card displaying information about a single hub.
|
||||||
class HubCard extends StatelessWidget {
|
class HubCard extends StatelessWidget {
|
||||||
|
|
||||||
/// Creates a [HubCard].
|
/// Creates a [HubCard].
|
||||||
const HubCard({
|
const HubCard({
|
||||||
required this.hub,
|
required this.hub,
|
||||||
required this.onNfcPressed,
|
required this.onNfcPressed,
|
||||||
required this.onDeletePressed,
|
required this.onDeletePressed,
|
||||||
|
required this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The hub to display.
|
/// The hub to display.
|
||||||
final Hub hub;
|
final Hub hub;
|
||||||
|
|
||||||
@@ -22,11 +23,16 @@ class HubCard extends StatelessWidget {
|
|||||||
/// Callback when the delete button is pressed.
|
/// Callback when the delete button is pressed.
|
||||||
final VoidCallback onDeletePressed;
|
final VoidCallback onDeletePressed;
|
||||||
|
|
||||||
|
/// Callback when the card is tapped.
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bool hasNfc = hub.nfcTagId != null;
|
final bool hasNfc = hub.nfcTagId != null;
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
@@ -116,6 +122,7 @@ class HubCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user