feat: Implement client hubs management feature, including CRUD operations and NFC tag assignment.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
49
apps/packages/features/client/hubs/lib/client_hubs.dart
Normal file
49
apps/packages/features/client/hubs/lib/client_hubs.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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/');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
apps/packages/features/client/hubs/pubspec.yaml
Normal file
37
apps/packages/features/client/hubs/pubspec.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user