feat: Implement client hubs management feature, including CRUD operations and NFC tag assignment.

This commit is contained in:
Achintha Isuru
2026-01-21 19:50:16 -05:00
parent 78917a5f84
commit 12dfde0551
27 changed files with 1670 additions and 5 deletions

View File

@@ -196,6 +196,40 @@
"clock_in_hubs": "Clock-In Hubs",
"billing_payments": "Billing & Payments"
}
},
"client_hubs": {
"title": "Hubs",
"subtitle": "Manage clock-in locations",
"add_hub": "Add Hub",
"empty_state": {
"title": "No hubs yet",
"description": "Create clock-in stations for your locations",
"button": "Add Your First Hub"
},
"about_hubs": {
"title": "About Hubs",
"description": "Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones."
},
"hub_card": {
"tag_label": "Tag: $id"
},
"add_hub_dialog": {
"title": "Add New Hub",
"name_label": "Hub Name *",
"name_hint": "e.g., Main Kitchen, Front Desk",
"location_label": "Location Name",
"location_hint": "e.g., Downtown Restaurant",
"address_label": "Address",
"address_hint": "Full address",
"create_button": "Create Hub"
},
"nfc_dialog": {
"title": "Identify NFC Tag",
"instruction": "Tap your phone to the NFC tag to identify it",
"scan_button": "Scan NFC Tag",
"tag_identified": "Tag Identified",
"assign_button": "Assign Tag"
}
}
}

View File

@@ -196,5 +196,39 @@
"clock_in_hubs": "Hubs de Marcaje",
"billing_payments": "Facturación y Pagos"
}
},
"client_hubs": {
"title": "Hubs",
"subtitle": "Gestionar ubicaciones de marcaje",
"add_hub": "Añadir Hub",
"empty_state": {
"title": "No hay hubs aún",
"description": "Crea estaciones de marcaje para tus ubicaciones",
"button": "Añade tu primer Hub"
},
"about_hubs": {
"title": "Sobre los Hubs",
"description": "Los Hubs son estaciones de marcaje en tus ubicaciones. Asigna etiquetas NFC a cada hub para que los trabajadores puedan marcar entrada/salida rápidamente usando sus teléfonos."
},
"hub_card": {
"tag_label": "Etiqueta: $id"
},
"add_hub_dialog": {
"title": "Añadir Nuevo Hub",
"name_label": "Nombre del Hub *",
"name_hint": "ej., Cocina Principal, Recepción",
"location_label": "Nombre de la Ubicación",
"location_hint": "ej., Restaurante Centro",
"address_label": "Dirección",
"address_hint": "Dirección completa",
"create_button": "Crear Hub"
},
"nfc_dialog": {
"title": "Identificar Etiqueta NFC",
"instruction": "Acerque su teléfono a la etiqueta NFC para identificarla",
"scan_button": "Escanear Etiqueta NFC",
"tag_identified": "Etiqueta Identificada",
"assign_button": "Asignar Etiqueta"
}
}
}

View File

@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 288 (144 per locale)
/// Strings: 332 (166 per locale)
///
/// Built on 2026-01-22 at 00:33 UTC
/// Built on 2026-01-22 at 00:48 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View File

@@ -46,6 +46,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final TranslationsClientAuthenticationEn client_authentication = TranslationsClientAuthenticationEn._(_root);
late final TranslationsClientHomeEn client_home = TranslationsClientHomeEn._(_root);
late final TranslationsClientSettingsEn client_settings = TranslationsClientSettingsEn._(_root);
late final TranslationsClientHubsEn client_hubs = TranslationsClientHubsEn._(_root);
}
// Path: common
@@ -138,6 +139,30 @@ class TranslationsClientSettingsEn {
late final TranslationsClientSettingsProfileEn profile = TranslationsClientSettingsProfileEn._(_root);
}
// Path: client_hubs
class TranslationsClientHubsEn {
TranslationsClientHubsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Hubs'
String get title => 'Hubs';
/// en: 'Manage clock-in locations'
String get subtitle => 'Manage clock-in locations';
/// en: 'Add Hub'
String get add_hub => 'Add Hub';
late final TranslationsClientHubsEmptyStateEn empty_state = TranslationsClientHubsEmptyStateEn._(_root);
late final TranslationsClientHubsAboutHubsEn about_hubs = TranslationsClientHubsAboutHubsEn._(_root);
late final TranslationsClientHubsHubCardEn hub_card = TranslationsClientHubsHubCardEn._(_root);
late final TranslationsClientHubsAddHubDialogEn add_hub_dialog = TranslationsClientHubsAddHubDialogEn._(_root);
late final TranslationsClientHubsNfcDialogEn nfc_dialog = TranslationsClientHubsNfcDialogEn._(_root);
}
// Path: staff_authentication.get_started_page
class TranslationsStaffAuthenticationGetStartedPageEn {
TranslationsStaffAuthenticationGetStartedPageEn._(this._root);
@@ -578,6 +603,108 @@ class TranslationsClientSettingsProfileEn {
String get billing_payments => 'Billing & Payments';
}
// Path: client_hubs.empty_state
class TranslationsClientHubsEmptyStateEn {
TranslationsClientHubsEmptyStateEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'No hubs yet'
String get title => 'No hubs yet';
/// en: 'Create clock-in stations for your locations'
String get description => 'Create clock-in stations for your locations';
/// en: 'Add Your First Hub'
String get button => 'Add Your First Hub';
}
// Path: client_hubs.about_hubs
class TranslationsClientHubsAboutHubsEn {
TranslationsClientHubsAboutHubsEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'About Hubs'
String get title => 'About Hubs';
/// en: 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.'
String get description => 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.';
}
// Path: client_hubs.hub_card
class TranslationsClientHubsHubCardEn {
TranslationsClientHubsHubCardEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Tag: $id'
String tag_label({required Object id}) => 'Tag: ${id}';
}
// Path: client_hubs.add_hub_dialog
class TranslationsClientHubsAddHubDialogEn {
TranslationsClientHubsAddHubDialogEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Add New Hub'
String get title => 'Add New Hub';
/// en: 'Hub Name *'
String get name_label => 'Hub Name *';
/// en: 'e.g., Main Kitchen, Front Desk'
String get name_hint => 'e.g., Main Kitchen, Front Desk';
/// en: 'Location Name'
String get location_label => 'Location Name';
/// en: 'e.g., Downtown Restaurant'
String get location_hint => 'e.g., Downtown Restaurant';
/// en: 'Address'
String get address_label => 'Address';
/// en: 'Full address'
String get address_hint => 'Full address';
/// en: 'Create Hub'
String get create_button => 'Create Hub';
}
// Path: client_hubs.nfc_dialog
class TranslationsClientHubsNfcDialogEn {
TranslationsClientHubsNfcDialogEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Identify NFC Tag'
String get title => 'Identify NFC Tag';
/// en: 'Tap your phone to the NFC tag to identify it'
String get instruction => 'Tap your phone to the NFC tag to identify it';
/// en: 'Scan NFC Tag'
String get scan_button => 'Scan NFC Tag';
/// en: 'Tag Identified'
String get tag_identified => 'Tag Identified';
/// en: 'Assign Tag'
String get assign_button => 'Assign Tag';
}
// Path: staff_authentication.profile_setup_page.steps
class TranslationsStaffAuthenticationProfileSetupPageStepsEn {
TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root);
@@ -898,6 +1025,28 @@ extension on Translations {
'client_settings.profile.quick_links' => 'Quick Links',
'client_settings.profile.clock_in_hubs' => 'Clock-In Hubs',
'client_settings.profile.billing_payments' => 'Billing & Payments',
'client_hubs.title' => 'Hubs',
'client_hubs.subtitle' => 'Manage clock-in locations',
'client_hubs.add_hub' => 'Add Hub',
'client_hubs.empty_state.title' => 'No hubs yet',
'client_hubs.empty_state.description' => 'Create clock-in stations for your locations',
'client_hubs.empty_state.button' => 'Add Your First Hub',
'client_hubs.about_hubs.title' => 'About Hubs',
'client_hubs.about_hubs.description' => 'Hubs are clock-in stations at your locations. Assign NFC tags to each hub so workers can quickly clock in/out using their phones.',
'client_hubs.hub_card.tag_label' => ({required Object id}) => 'Tag: ${id}',
'client_hubs.add_hub_dialog.title' => 'Add New Hub',
'client_hubs.add_hub_dialog.name_label' => 'Hub Name *',
'client_hubs.add_hub_dialog.name_hint' => 'e.g., Main Kitchen, Front Desk',
'client_hubs.add_hub_dialog.location_label' => 'Location Name',
'client_hubs.add_hub_dialog.location_hint' => 'e.g., Downtown Restaurant',
'client_hubs.add_hub_dialog.address_label' => 'Address',
'client_hubs.add_hub_dialog.address_hint' => 'Full address',
'client_hubs.add_hub_dialog.create_button' => 'Create Hub',
'client_hubs.nfc_dialog.title' => 'Identify NFC Tag',
'client_hubs.nfc_dialog.instruction' => 'Tap your phone to the NFC tag to identify it',
'client_hubs.nfc_dialog.scan_button' => 'Scan NFC Tag',
'client_hubs.nfc_dialog.tag_identified' => 'Tag Identified',
'client_hubs.nfc_dialog.assign_button' => 'Assign Tag',
_ => null,
};
}

View File

@@ -43,6 +43,7 @@ class TranslationsEs with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsClientAuthenticationEs client_authentication = _TranslationsClientAuthenticationEs._(_root);
@override late final _TranslationsClientHomeEs client_home = _TranslationsClientHomeEs._(_root);
@override late final _TranslationsClientSettingsEs client_settings = _TranslationsClientSettingsEs._(_root);
@override late final _TranslationsClientHubsEs client_hubs = _TranslationsClientHubsEs._(_root);
}
// Path: common
@@ -121,6 +122,23 @@ class _TranslationsClientSettingsEs implements TranslationsClientSettingsEn {
@override late final _TranslationsClientSettingsProfileEs profile = _TranslationsClientSettingsProfileEs._(_root);
}
// Path: client_hubs
class _TranslationsClientHubsEs implements TranslationsClientHubsEn {
_TranslationsClientHubsEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Hubs';
@override String get subtitle => 'Gestionar ubicaciones de marcaje';
@override String get add_hub => 'Añadir Hub';
@override late final _TranslationsClientHubsEmptyStateEs empty_state = _TranslationsClientHubsEmptyStateEs._(_root);
@override late final _TranslationsClientHubsAboutHubsEs about_hubs = _TranslationsClientHubsAboutHubsEs._(_root);
@override late final _TranslationsClientHubsHubCardEs hub_card = _TranslationsClientHubsHubCardEs._(_root);
@override late final _TranslationsClientHubsAddHubDialogEs add_hub_dialog = _TranslationsClientHubsAddHubDialogEs._(_root);
@override late final _TranslationsClientHubsNfcDialogEs nfc_dialog = _TranslationsClientHubsNfcDialogEs._(_root);
}
// Path: staff_authentication.get_started_page
class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn {
_TranslationsStaffAuthenticationGetStartedPageEs._(this._root);
@@ -360,6 +378,70 @@ class _TranslationsClientSettingsProfileEs implements TranslationsClientSettings
@override String get billing_payments => 'Facturación y Pagos';
}
// Path: client_hubs.empty_state
class _TranslationsClientHubsEmptyStateEs implements TranslationsClientHubsEmptyStateEn {
_TranslationsClientHubsEmptyStateEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'No hay hubs aún';
@override String get description => 'Crea estaciones de marcaje para tus ubicaciones';
@override String get button => 'Añade tu primer Hub';
}
// Path: client_hubs.about_hubs
class _TranslationsClientHubsAboutHubsEs implements TranslationsClientHubsAboutHubsEn {
_TranslationsClientHubsAboutHubsEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Sobre los Hubs';
@override String get description => 'Los Hubs son estaciones de marcaje en tus ubicaciones. Asigna etiquetas NFC a cada hub para que los trabajadores puedan marcar entrada/salida rápidamente usando sus teléfonos.';
}
// Path: client_hubs.hub_card
class _TranslationsClientHubsHubCardEs implements TranslationsClientHubsHubCardEn {
_TranslationsClientHubsHubCardEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String tag_label({required Object id}) => 'Etiqueta: ${id}';
}
// Path: client_hubs.add_hub_dialog
class _TranslationsClientHubsAddHubDialogEs implements TranslationsClientHubsAddHubDialogEn {
_TranslationsClientHubsAddHubDialogEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Añadir Nuevo Hub';
@override String get name_label => 'Nombre del Hub *';
@override String get name_hint => 'ej., Cocina Principal, Recepción';
@override String get location_label => 'Nombre de la Ubicación';
@override String get location_hint => 'ej., Restaurante Centro';
@override String get address_label => 'Dirección';
@override String get address_hint => 'Dirección completa';
@override String get create_button => 'Crear Hub';
}
// Path: client_hubs.nfc_dialog
class _TranslationsClientHubsNfcDialogEs implements TranslationsClientHubsNfcDialogEn {
_TranslationsClientHubsNfcDialogEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Identificar Etiqueta NFC';
@override String get instruction => 'Acerque su teléfono a la etiqueta NFC para identificarla';
@override String get scan_button => 'Escanear Etiqueta NFC';
@override String get tag_identified => 'Etiqueta Identificada';
@override String get assign_button => 'Asignar Etiqueta';
}
// Path: staff_authentication.profile_setup_page.steps
class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn {
_TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root);
@@ -605,6 +687,28 @@ extension on TranslationsEs {
'client_settings.profile.quick_links' => 'Enlaces rápidos',
'client_settings.profile.clock_in_hubs' => 'Hubs de Marcaje',
'client_settings.profile.billing_payments' => 'Facturación y Pagos',
'client_hubs.title' => 'Hubs',
'client_hubs.subtitle' => 'Gestionar ubicaciones de marcaje',
'client_hubs.add_hub' => 'Añadir Hub',
'client_hubs.empty_state.title' => 'No hay hubs aún',
'client_hubs.empty_state.description' => 'Crea estaciones de marcaje para tus ubicaciones',
'client_hubs.empty_state.button' => 'Añade tu primer Hub',
'client_hubs.about_hubs.title' => 'Sobre los Hubs',
'client_hubs.about_hubs.description' => 'Los Hubs son estaciones de marcaje en tus ubicaciones. Asigna etiquetas NFC a cada hub para que los trabajadores puedan marcar entrada/salida rápidamente usando sus teléfonos.',
'client_hubs.hub_card.tag_label' => ({required Object id}) => 'Etiqueta: ${id}',
'client_hubs.add_hub_dialog.title' => 'Añadir Nuevo Hub',
'client_hubs.add_hub_dialog.name_label' => 'Nombre del Hub *',
'client_hubs.add_hub_dialog.name_hint' => 'ej., Cocina Principal, Recepción',
'client_hubs.add_hub_dialog.location_label' => 'Nombre de la Ubicación',
'client_hubs.add_hub_dialog.location_hint' => 'ej., Restaurante Centro',
'client_hubs.add_hub_dialog.address_label' => 'Dirección',
'client_hubs.add_hub_dialog.address_hint' => 'Dirección completa',
'client_hubs.add_hub_dialog.create_button' => 'Crear Hub',
'client_hubs.nfc_dialog.title' => 'Identificar Etiqueta NFC',
'client_hubs.nfc_dialog.instruction' => 'Acerque su teléfono a la etiqueta NFC para identificarla',
'client_hubs.nfc_dialog.scan_button' => 'Escanear Etiqueta NFC',
'client_hubs.nfc_dialog.tag_identified' => 'Etiqueta Identificada',
'client_hubs.nfc_dialog.assign_button' => 'Asignar Etiqueta',
_ => null,
};
}

View File

@@ -25,4 +25,30 @@ class BusinessRepositoryMock {
),
];
}
}
Future<Hub> createHub({
required String businessId,
required String name,
required String address,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
return Hub(
id: 'hub_${DateTime.now().millisecondsSinceEpoch}',
businessId: businessId,
name: name,
address: address,
status: HubStatus.active,
);
}
Future<void> deleteHub(String id) async {
await Future.delayed(const Duration(milliseconds: 300));
}
Future<void> assignNfcTag({
required String hubId,
required String nfcTagId,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
}
}

View File

@@ -26,6 +26,9 @@ class Hub extends Equatable {
/// Physical address of this hub.
final String address;
/// Unique identifier of the NFC tag assigned to this hub.
final String? nfcTagId;
/// Operational status.
final HubStatus status;
@@ -34,9 +37,10 @@ class Hub extends Equatable {
required this.businessId,
required this.name,
required this.address,
this.nfcTagId,
required this.status,
});
@override
List<Object?> get props => [id, businessId, name, address, status];
}
List<Object?> get props => [id, businessId, name, address, nfcTagId, status];
}

View File

@@ -0,0 +1,49 @@
library client_hubs;
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'src/data/repositories_impl/hub_repository_impl.dart';
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_hubs_usecase.dart';
import 'src/presentation/blocs/client_hubs_bloc.dart';
import 'src/presentation/pages/client_hubs_page.dart';
export 'src/presentation/pages/client_hubs_page.dart';
/// A [Module] for the client hubs feature.
class ClientHubsModule extends Module {
@override
List<Module> get imports => [DataConnectModule()];
@override
void binds(Injector i) {
// Repositories
i.addLazySingleton<HubRepositoryInterface>(
() => HubRepositoryImpl(mock: i.get<BusinessRepositoryMock>()),
);
// UseCases
i.addLazySingleton(GetHubsUseCase.new);
i.addLazySingleton(CreateHubUseCase.new);
i.addLazySingleton(DeleteHubUseCase.new);
i.addLazySingleton(AssignNfcTagUseCase.new);
// BLoCs
i.add<ClientHubsBloc>(
() => ClientHubsBloc(
getHubsUseCase: i.get<GetHubsUseCase>(),
createHubUseCase: i.get<CreateHubUseCase>(),
deleteHubUseCase: i.get<DeleteHubUseCase>(),
assignNfcTagUseCase: i.get<AssignNfcTagUseCase>(),
),
);
}
@override
void routes(RouteManager r) {
r.child('/', child: (_) => const ClientHubsPage());
}
}

View File

@@ -0,0 +1,37 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface].
///
/// This implementation delegates data access to the [BusinessRepositoryMock]
/// from the `data_connect` package.
class HubRepositoryImpl implements HubRepositoryInterface {
/// The business repository mock from data connect.
final BusinessRepositoryMock mock;
/// Creates a [HubRepositoryImpl] with the required [mock].
HubRepositoryImpl({required this.mock});
@override
Future<List<Hub>> getHubs() {
// In a real app, we would get the business ID from a session or state.
// For this prototype/mock, we use a hardcoded value.
return mock.getHubs('biz_1');
}
@override
Future<Hub> createHub({required String name, required String address}) {
return mock.createHub(businessId: 'biz_1', name: name, address: address);
}
@override
Future<void> deleteHub(String id) {
return mock.deleteHub(id);
}
@override
Future<void> assignNfcTag({required String hubId, required String nfcTagId}) {
return mock.assignNfcTag(hubId: hubId, nfcTagId: nfcTagId);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:krow_core/core.dart';
/// Arguments for creating a new hub.
class CreateHubArguments extends UseCaseArgument {
final String name;
final String address;
const CreateHubArguments({required this.name, required this.address});
@override
List<Object?> get props => [name, address];
}
/// Arguments for assigning an NFC tag to a hub.
class AssignNfcTagArguments extends UseCaseArgument {
final String hubId;
final String nfcTagId;
const AssignNfcTagArguments({required this.hubId, required this.nfcTagId});
@override
List<Object?> get props => [hubId, nfcTagId];
}
/// Arguments for deleting a hub.
class DeleteHubArguments extends UseCaseArgument {
final String hubId;
const DeleteHubArguments({required this.hubId});
@override
List<Object?> get props => [hubId];
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_domain/krow_domain.dart';
/// Interface for the Hub repository.
///
/// This repository handles hub-related operations such as
/// fetching, creating, deleting hubs and assigning NFC tags.
abstract interface class HubRepositoryInterface {
/// Fetches the list of hubs for the current client.
Future<List<Hub>> getHubs();
/// Creates a new hub.
Future<Hub> createHub({required String name, required String address});
/// Deletes a hub by its [id].
Future<void> deleteHub(String id);
/// Assigns an NFC tag to a hub.
Future<void> assignNfcTag({required String hubId, required String nfcTagId});
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
import '../arguments/hub_arguments.dart';
import '../repositories/hub_repository_interface.dart';
/// Use case for assigning an NFC tag to a hub.
class AssignNfcTagUseCase implements UseCase<AssignNfcTagArguments, void> {
final HubRepositoryInterface _repository;
AssignNfcTagUseCase(this._repository);
@override
Future<void> call(AssignNfcTagArguments arguments) {
return _repository.assignNfcTag(
hubId: arguments.hubId,
nfcTagId: arguments.nfcTagId,
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/hub_arguments.dart';
import '../repositories/hub_repository_interface.dart';
/// Use case for creating a new hub.
class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
final HubRepositoryInterface _repository;
CreateHubUseCase(this._repository);
@override
Future<Hub> call(CreateHubArguments arguments) {
return _repository.createHub(
name: arguments.name,
address: arguments.address,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow_core/core.dart';
import '../arguments/hub_arguments.dart';
import '../repositories/hub_repository_interface.dart';
/// Use case for deleting a hub.
class DeleteHubUseCase implements UseCase<DeleteHubArguments, void> {
final HubRepositoryInterface _repository;
DeleteHubUseCase(this._repository);
@override
Future<void> call(DeleteHubArguments arguments) {
return _repository.deleteHub(arguments.hubId);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/hub_repository_interface.dart';
/// Use case for fetching the list of hubs.
class GetHubsUseCase implements NoInputUseCase<List<Hub>> {
final HubRepositoryInterface _repository;
GetHubsUseCase(this._repository);
@override
Future<List<Hub>> call() {
return _repository.getHubs();
}
}

View File

@@ -0,0 +1,154 @@
import 'package:bloc/bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../domain/arguments/hub_arguments.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/get_hubs_usecase.dart';
import 'client_hubs_event.dart';
import 'client_hubs_state.dart';
/// BLoC responsible for managing client hubs.
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
implements Disposable {
final GetHubsUseCase _getHubsUseCase;
final CreateHubUseCase _createHubUseCase;
final DeleteHubUseCase _deleteHubUseCase;
final AssignNfcTagUseCase _assignNfcTagUseCase;
ClientHubsBloc({
required GetHubsUseCase getHubsUseCase,
required CreateHubUseCase createHubUseCase,
required DeleteHubUseCase deleteHubUseCase,
required AssignNfcTagUseCase assignNfcTagUseCase,
}) : _getHubsUseCase = getHubsUseCase,
_createHubUseCase = createHubUseCase,
_deleteHubUseCase = deleteHubUseCase,
_assignNfcTagUseCase = assignNfcTagUseCase,
super(const ClientHubsState()) {
on<ClientHubsFetched>(_onFetched);
on<ClientHubsAddRequested>(_onAddRequested);
on<ClientHubsDeleteRequested>(_onDeleteRequested);
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
on<ClientHubsMessageCleared>(_onMessageCleared);
}
Future<void> _onFetched(
ClientHubsFetched event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.loading));
try {
final hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
} catch (e) {
emit(
state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onAddRequested(
ClientHubsAddRequested event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _createHubUseCase(
CreateHubArguments(name: event.name, address: event.address),
);
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub created successfully',
),
);
} catch (e) {
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onDeleteRequested(
ClientHubsDeleteRequested event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub deleted successfully',
),
);
} catch (e) {
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onNfcTagAssignRequested(
ClientHubsNfcTagAssignRequested event,
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
);
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'NFC tag assigned successfully',
),
);
} catch (e) {
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
),
);
}
}
void _onMessageCleared(
ClientHubsMessageCleared event,
Emitter<ClientHubsState> emit,
) {
emit(
state.copyWith(
errorMessage: null,
successMessage: null,
status:
state.status == ClientHubsStatus.actionSuccess ||
state.status == ClientHubsStatus.actionFailure
? ClientHubsStatus.success
: state.status,
),
);
}
@override
void dispose() {
close();
}
}

View File

@@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
/// Base class for all client hubs events.
abstract class ClientHubsEvent extends Equatable {
const ClientHubsEvent();
@override
List<Object?> get props => [];
}
/// Event triggered to fetch the list of hubs.
class ClientHubsFetched extends ClientHubsEvent {
const ClientHubsFetched();
}
/// Event triggered to add a new hub.
class ClientHubsAddRequested extends ClientHubsEvent {
final String name;
final String address;
const ClientHubsAddRequested({required this.name, required this.address});
@override
List<Object?> get props => [name, address];
}
/// Event triggered to delete a hub.
class ClientHubsDeleteRequested extends ClientHubsEvent {
final String hubId;
const ClientHubsDeleteRequested(this.hubId);
@override
List<Object?> get props => [hubId];
}
/// Event triggered to assign an NFC tag to a hub.
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
final String hubId;
final String nfcTagId;
const ClientHubsNfcTagAssignRequested({
required this.hubId,
required this.nfcTagId,
});
@override
List<Object?> get props => [hubId, nfcTagId];
}
/// Event triggered to clear any error or success messages.
class ClientHubsMessageCleared extends ClientHubsEvent {
const ClientHubsMessageCleared();
}

View File

@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Enum representing the status of the client hubs state.
enum ClientHubsStatus {
initial,
loading,
success,
failure,
actionInProgress,
actionSuccess,
actionFailure,
}
/// State class for the ClientHubs BLoC.
class ClientHubsState extends Equatable {
final ClientHubsStatus status;
final List<Hub> hubs;
final String? errorMessage;
final String? successMessage;
const ClientHubsState({
this.status = ClientHubsStatus.initial,
this.hubs = const [],
this.errorMessage,
this.successMessage,
});
ClientHubsState copyWith({
ClientHubsStatus? status,
List<Hub>? hubs,
String? errorMessage,
String? successMessage,
}) {
return ClientHubsState(
status: status ?? this.status,
hubs: hubs ?? this.hubs,
errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage,
);
}
@override
List<Object?> get props => [status, hubs, errorMessage, successMessage];
}

View File

@@ -0,0 +1,9 @@
import 'package:flutter_modular/flutter_modular.dart';
/// Extension on [IModularNavigator] to provide typed navigation for client hubs.
extension ClientHubsNavigator on IModularNavigator {
/// Navigates to the client hubs page.
Future<void> pushClientHubs() async {
await pushNamed('/client/hubs/');
}
}

View File

@@ -0,0 +1,202 @@
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:lucide_icons/lucide_icons.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
import '../blocs/client_hubs_bloc.dart';
import '../blocs/client_hubs_event.dart';
import '../blocs/client_hubs_state.dart';
import '../widgets/add_hub_dialog.dart';
import '../widgets/hub_card.dart';
import '../widgets/hub_empty_state.dart';
import '../widgets/hub_info_card.dart';
import '../widgets/identify_nfc_dialog.dart';
/// The main page for the client hubs feature.
class ClientHubsPage extends StatefulWidget {
/// Creates a [ClientHubsPage].
const ClientHubsPage({super.key});
@override
State<ClientHubsPage> createState() => _ClientHubsPageState();
}
class _ClientHubsPageState extends State<ClientHubsPage> {
bool _showAddHub = false;
Hub? _hubToIdentify;
@override
Widget build(BuildContext context) {
return BlocProvider<ClientHubsBloc>(
create: (context) =>
Modular.get<ClientHubsBloc>()..add(const ClientHubsFetched()),
child: BlocConsumer<ClientHubsBloc, ClientHubsState>(
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.errorMessage!)));
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsMessageCleared());
}
if (state.successMessage != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.successMessage!)));
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsMessageCleared());
}
},
builder: (context, state) {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), // slate-50
body: Stack(
children: [
CustomScrollView(
slivers: [
_buildAppBar(context),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
sliver: SliverList(
delegate: SliverChildListDelegate([
if (state.status == ClientHubsStatus.loading)
const Center(child: CircularProgressIndicator())
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () =>
setState(() => _showAddHub = true),
)
else ...[
...state.hubs.map(
(hub) => HubCard(
hub: hub,
onNfcPressed: () =>
setState(() => _hubToIdentify = hub),
onDeletePressed: () =>
BlocProvider.of<ClientHubsBloc>(
context,
).add(ClientHubsDeleteRequested(hub.id)),
),
),
],
const SizedBox(height: 20),
const HubInfoCard(),
]),
),
),
],
),
if (_showAddHub)
AddHubDialog(
onCreate: (name, address) {
BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsAddRequested(name: name, address: address),
);
setState(() => _showAddHub = false);
},
onCancel: () => setState(() => _showAddHub = false),
),
if (_hubToIdentify != null)
IdentifyNfcDialog(
hub: _hubToIdentify!,
onAssign: (tagId) {
BlocProvider.of<ClientHubsBloc>(context).add(
ClientHubsNfcTagAssignRequested(
hubId: _hubToIdentify!.id,
nfcTagId: tagId,
),
);
setState(() => _hubToIdentify = null);
},
onCancel: () => setState(() => _hubToIdentify = null),
),
if (state.status == ClientHubsStatus.actionInProgress)
Container(
color: Colors.black.withOpacity(0.1),
child: const Center(child: CircularProgressIndicator()),
),
],
),
);
},
),
);
}
Widget _buildAppBar(BuildContext context) {
return SliverAppBar(
backgroundColor: const Color(0xFF121826),
automaticallyImplyLeading: false,
expandedHeight: 140,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF121826), Color(0xFF1E293B)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: const EdgeInsets.fromLTRB(20, 48, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => Modular.to.pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.arrowLeft,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.client_hubs.title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
Text(
t.client_hubs.subtitle,
style: const TextStyle(
color: Color(0xFFCBD5E1), // slate-300
fontSize: 14,
),
),
],
),
UiButton.primary(
onPressed: () => setState(() => _showAddHub = true),
text: t.client_hubs.add_hub,
leadingIcon: LucideIcons.plus,
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,154 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
/// A dialog for adding a new hub.
class AddHubDialog extends StatefulWidget {
/// Callback when the "Create Hub" button is pressed.
final Function(String name, String address) onCreate;
/// Callback when the dialog is cancelled.
final VoidCallback onCancel;
/// Creates an [AddHubDialog].
const AddHubDialog({
required this.onCreate,
required this.onCancel,
super.key,
});
@override
State<AddHubDialog> createState() => _AddHubDialogState();
}
class _AddHubDialogState extends State<AddHubDialog> {
late final TextEditingController _nameController;
late final TextEditingController _addressController;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
_addressController = TextEditingController();
}
@override
void dispose() {
_nameController.dispose();
_addressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withOpacity(0.5),
child: Center(
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
t.client_hubs.add_hub_dialog.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 24),
_buildFieldLabel(t.client_hubs.add_hub_dialog.name_label),
TextField(
controller: _nameController,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.name_hint,
),
),
const SizedBox(height: 16),
_buildFieldLabel(t.client_hubs.add_hub_dialog.address_label),
TextField(
controller: _addressController,
decoration: _buildInputDecoration(
t.client_hubs.add_hub_dialog.address_hint,
),
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: UiButton.secondary(
onPressed: widget.onCancel,
text: t.common.cancel,
),
),
const SizedBox(width: 12),
Expanded(
child: UiButton.primary(
onPressed: () {
if (_nameController.text.isNotEmpty) {
widget.onCreate(
_nameController.text,
_addressController.text,
);
}
},
text: t.client_hubs.add_hub_dialog.create_button,
),
),
],
),
],
),
),
),
),
);
}
Widget _buildFieldLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF0F172A),
),
),
);
}
InputDecoration _buildInputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14),
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF2563EB), width: 2),
),
);
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// A card displaying information about a single hub.
class HubCard extends StatelessWidget {
/// The hub to display.
final Hub hub;
/// Callback when the NFC button is pressed.
final VoidCallback onNfcPressed;
/// Callback when the delete button is pressed.
final VoidCallback onDeletePressed;
/// Creates a [HubCard].
const HubCard({
required this.hub,
required this.onNfcPressed,
required this.onDeletePressed,
super.key,
});
@override
Widget build(BuildContext context) {
final bool hasNfc = hub.nfcTagId != null;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50
borderRadius: BorderRadius.circular(16),
),
child: Icon(
hasNfc ? LucideIcons.checkCircle : LucideIcons.nfc,
color: hasNfc
? const Color(0xFF16A34A)
: const Color(0xFF94A3B8), // green-600 or slate-400
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
hub.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF0F172A),
),
),
if (hub.address.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
const Icon(
LucideIcons.mapPin,
size: 12,
color: Color(0xFF94A3B8),
),
const SizedBox(width: 4),
Expanded(
child: Text(
hub.address,
style: const TextStyle(
color: Color(0xFF64748B),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (hasNfc)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
t.client_hubs.hub_card.tag_label(id: hub.nfcTagId!),
style: const TextStyle(
color: Color(0xFF16A34A),
fontSize: 12,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
),
],
),
),
Row(
children: [
IconButton(
onPressed: onNfcPressed,
icon: const Icon(
LucideIcons.nfc,
color: Color(0xFF2563EB),
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
const SizedBox(width: 8),
IconButton(
onPressed: onDeletePressed,
icon: const Icon(
LucideIcons.trash2,
color: Color(0xFFDC2626),
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// Widget displayed when there are no hubs.
class HubEmptyState extends StatelessWidget {
/// Callback when the add button is pressed.
final VoidCallback onAddPressed;
/// Creates a [HubEmptyState].
const HubEmptyState({required this.onAddPressed, super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: Color(0xFFF1F5F9), // slate-100
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.nfc,
size: 32,
color: Color(0xFF94A3B8),
),
),
const SizedBox(height: 16),
Text(
t.client_hubs.empty_state.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 8),
Text(
t.client_hubs.empty_state.description,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF64748B), fontSize: 14),
),
const SizedBox(height: 24),
UiButton.primary(
onPressed: onAddPressed,
text: t.client_hubs.empty_state.button,
leadingIcon: LucideIcons.plus,
),
],
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
/// A card with information about how hubs work.
class HubInfoCard extends StatelessWidget {
/// Creates a [HubInfoCard].
const HubInfoCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF), // blue-50
borderRadius: BorderRadius.circular(16),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(LucideIcons.nfc, size: 20, color: Color(0xFF2563EB)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.client_hubs.about_hubs.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 4),
Text(
t.client_hubs.about_hubs.description,
style: const TextStyle(
color: Color(0xFF334155),
fontSize: 12,
height: 1.4,
),
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,186 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
/// A dialog for identifying and assigning an NFC tag to a hub.
class IdentifyNfcDialog extends StatefulWidget {
/// The hub to assign the tag to.
final Hub hub;
/// Callback when a tag is assigned.
final Function(String nfcTagId) onAssign;
/// Callback when the dialog is cancelled.
final VoidCallback onCancel;
/// Creates an [IdentifyNfcDialog].
const IdentifyNfcDialog({
required this.hub,
required this.onAssign,
required this.onCancel,
super.key,
});
@override
State<IdentifyNfcDialog> createState() => _IdentifyNfcDialogState();
}
class _IdentifyNfcDialogState extends State<IdentifyNfcDialog> {
String? _nfcTagId;
void _simulateNFCScan() {
setState(() {
_nfcTagId =
'NFC-${DateTime.now().millisecondsSinceEpoch.toString().substring(8).toUpperCase()}';
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withOpacity(0.5),
child: Center(
child: SingleChildScrollView(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
t.client_hubs.nfc_dialog.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 32),
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Color(0xFFEFF6FF), // blue-50
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.nfc,
size: 40,
color: Color(0xFF2563EB),
),
),
const SizedBox(height: 16),
Text(
widget.hub.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 8),
Text(
t.client_hubs.nfc_dialog.instruction,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF64748B),
fontSize: 14,
),
),
const SizedBox(height: 24),
UiButton.secondary(
onPressed: _simulateNFCScan,
text: t.client_hubs.nfc_dialog.scan_button,
leadingIcon: LucideIcons.nfc,
),
if (_nfcTagId != null) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4), // green-50
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
LucideIcons.checkCircle,
size: 20,
color: Color(0xFF16A34A),
),
const SizedBox(width: 8),
Text(
t.client_hubs.nfc_dialog.tag_identified,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xFF0F172A),
),
),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFDCE8E0)),
),
child: Text(
_nfcTagId!,
style: const TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
fontSize: 12,
color: Color(0xFF334155),
),
),
),
],
),
),
],
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: UiButton.secondary(
onPressed: widget.onCancel,
text: t.common.cancel,
),
),
const SizedBox(width: 12),
Expanded(
child: UiButton.primary(
onPressed: _nfcTagId != null
? () => widget.onAssign(_nfcTagId!)
: null,
text: t.client_hubs.nfc_dialog.assign_button,
),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,37 @@
name: client_hubs
description: "Client hubs management feature for the KROW platform."
version: 0.0.1
publish_to: none
resolution: workspace
environment:
sdk: '>=3.10.0 <4.0.0'
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.0
flutter_modular: ^6.3.2
equatable: ^2.0.5
lucide_icons: ^0.257.0
# KROW Packages
krow_core:
path: ../../../core
krow_domain:
path: ../../../domain
krow_data_connect:
path: ../../../data_connect
design_system:
path: ../../../design_system
core_localization:
path: ../../../core_localization
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true

View File

@@ -13,6 +13,7 @@ workspace:
- packages/features/client/authentication
- packages/features/client/home
- packages/features/client/settings
- packages/features/client/hubs
- apps/staff
- apps/client
- apps/design_system_viewer