From 94e2fcbf7dce09fe756e6a10329d40c48989b8f6 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 22 Apr 2026 22:14:06 +0200 Subject: [PATCH] refactor(device_management): migrate contacts to Riverpod --- .../presentation/contacts_screen.dart | 174 +++++++--- .../presentation/edit_contact_screen.dart | 276 +++++++++++----- .../providers/contacts_controller.dart | 115 +++++++ .../providers/contacts_controller.g.dart | 56 ++++ .../contacts_editing_mode_provider.dart | 16 + .../contacts_editing_mode_provider.g.dart | 64 ++++ .../providers/contacts_provider.dart | 26 ++ .../providers/contacts_provider.g.dart | 87 +++++ .../providers/edit_contact_form_provider.dart | 46 +++ .../edit_contact_form_provider.g.dart | 108 ++++++ .../providers/new_contact_form_provider.dart | 44 +++ .../new_contact_form_provider.g.dart | 63 ++++ .../state/contacts_view_model.dart | 126 ------- .../state/contacts_view_state.dart | 18 - .../state/contacts_view_state.freezed.dart | 292 ----------------- .../state/edit_contact_view_model.dart | 111 ------- .../state/edit_contact_view_state.dart | 17 - .../edit_contact_view_state.freezed.dart | 307 ------------------ .../state/new_contact_view_model.dart | 139 -------- .../state/new_contact_view_state.dart | 14 - .../state/new_contact_view_state.freezed.dart | 277 ---------------- .../widgets/confirm_delete_dialog.dart | 74 ----- .../presentation/widgets/contact_card.dart | 19 +- .../widgets/new_contact_dialog.dart | 178 +++++++--- .../contacts/contacts_controller_test.dart | 173 ++++++++++ packages/sf_localizations/assets/l10n/de.json | 3 + packages/sf_localizations/assets/l10n/en.json | 3 + packages/sf_localizations/assets/l10n/es.json | 3 + packages/sf_localizations/assets/l10n/fr.json | 3 + packages/sf_localizations/assets/l10n/it.json | 3 + packages/sf_localizations/assets/l10n/pt.json | 3 + .../lib/src/generated/i18n.dart | 3 + 32 files changed, 1278 insertions(+), 1563 deletions(-) create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.g.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.freezed.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_model.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.freezed.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_model.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.freezed.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/confirm_delete_dialog.dart create mode 100644 modules/legacy/modules/device_management/test/features/contacts/contacts_controller_test.dart diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart index ed63c1c6..d050b171 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart @@ -1,17 +1,19 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart'; +import 'package:device_management/src/features/contacts/presentation/widgets/contact_card.dart'; +import 'package:device_management/src/features/contacts/presentation/widgets/new_contact_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_ui/legacy_ui.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import 'state/contacts_view_model.dart'; -import 'widgets/contact_card.dart'; -import 'widgets/new_contact_dialog.dart'; - class ContactsScreen extends ConsumerWidget { final NavigationContract navigationContract; @@ -19,61 +21,106 @@ class ContactsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(contactsViewModelProvider.notifier); - final state = ref.watch(contactsViewModelProvider); + final primaryColor = context.sfColors.legacyPrimary; + final isEditing = ref.watch(contactsEditingModeProvider); + final userAsync = ref.watch(userInfoProvider); + final device = ref.watch(selectedDeviceProvider).value; + final maxContacts = + device?.capabilities?.contacts?.maxForType('white') ?? 10; - ref.listen(contactsViewModelProvider.select((s) => s.error), (prev, next) { - if (next == null) return; - showTopSnackbar( - context, - message: context.translate(next.i18nKey), - type: MessageType.error, - ); - vm.clearError(); + ref.listen(contactsControllerProvider, (prev, next) async { + if (prev == null || !prev.isLoading || next.isLoading) return; + if (next.hasError) { + final error = next.error; + if (error is ContactsMinReachedException) { + await showErrorDialog(context, I18n.errorContactsMin); + } else if (error is ContactsMaxReachedException) { + await showErrorDialog(context, I18n.errorContactsMax); + } else { + await next.showErrorOn(context); + } + return; + } + final action = + ref.read(contactsControllerProvider.notifier).lastAction; + final key = switch (action) { + ContactsAction.create => I18n.contactAdded, + ContactsAction.update => I18n.contactUpdated, + ContactsAction.delete => I18n.contactDeleted, + null => I18n.deviceUpdatedSuccess, + }; + await showSuccessDialog(context, key); }); return LegacyPageLayout( title: context.translate(I18n.contactsAgendaTitle), showEdit: true, - onEditChange: vm.toggleIsEditing, - body: state.isLoading - ? const Center(child: CircularProgressIndicator()) - : state.contacts.isEmpty - ? const _EmptyState() - : ListView.separated( - padding: EdgeInsets.symmetric( - horizontal: SizeUtils.getByScreen(small: 22, big: 21), - vertical: SizeUtils.getByScreen(small: 10, big: 8), - ), - itemBuilder: (_, index) => ContactCard( - contact: state.contacts[index], - isEditing: state.isEditing, - ), - separatorBuilder: (_, __) => - SizedBox(height: SizeUtils.getByScreen(small: 18, big: 17)), - itemCount: state.contacts.length, + onEditChange: ref.read(contactsEditingModeProvider.notifier).toggle, + body: userAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text(context.translate(I18n.errorGeneric)), + ), + data: (user) { + final contactsAsync = ref.watch(contactsProvider(user.id)); + return contactsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text(context.translate(I18n.errorGeneric)), ), + data: (contacts) { + if (contacts.isEmpty) return const _EmptyState(); + return ListView.separated( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 22, big: 21), + vertical: SizeUtils.getByScreen(small: 10, big: 8), + ), + itemBuilder: (_, index) => ContactCard( + contact: contacts[index], + isEditing: isEditing, + onDelete: () => _confirmDelete( + context, + ref, + userId: user.id, + contactId: contacts[index].id, + currentCount: contacts.length, + ), + ), + separatorBuilder: (_, __) => SizedBox( + height: SizeUtils.getByScreen(small: 18, big: 17), + ), + itemCount: contacts.length, + ); + }, + ); + }, + ), footer: Material( - color: context.sfColors.legacyPrimary, + color: primaryColor, shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), onTap: () async { if (!await guardDeviceCommand(context, ref)) return; if (!context.mounted) return; - if (state.contacts.length >= state.maxContacts) { - showTopSnackbar( - context, - message: context.translate(I18n.errorContactsMax), - ); + final user = userAsync.value; + if (user == null) return; + final contacts = + ref.read(contactsProvider(user.id)).value ?? const []; + if (contacts.length >= maxContacts) { + await showErrorDialog(context, I18n.errorContactsMax); return; } - - showDialog( + if (!context.mounted) return; + showDialog( context: context, - builder: (_) => const Dialog( + builder: (_) => Dialog( backgroundColor: Colors.transparent, - child: NewContactDialog(), + child: NewContactDialog( + userId: user.id, + currentCount: contacts.length, + maxContacts: maxContacts, + ), ), ); }, @@ -90,10 +137,43 @@ class ContactsScreen extends ConsumerWidget { ), ); } + + Future _confirmDelete( + BuildContext context, + WidgetRef ref, { + required String userId, + required String contactId, + required int currentCount, + }) async { + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.translate(I18n.deleteContactMessage)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(context.translate(I18n.cancel)), + ), + FilledButton( + onPressed: () => Navigator.pop(dialogContext, true), + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + ), + child: Text(context.translate(I18n.delete)), + ), + ], + ), + ); + if (confirmed != true) return; + ref.read(contactsControllerProvider.notifier).deleteContact( + userId: userId, + contactId: contactId, + currentCount: currentCount, + ); + } } class _EmptyState extends StatelessWidget { - const _EmptyState(); @override @@ -105,8 +185,8 @@ class _EmptyState extends StatelessWidget { Icon( SFIcons.contactsCircle, size: SizeUtils.getByScreen(small: 64, big: 60), - color: Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.3), + color: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), ), SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), Text( @@ -122,7 +202,9 @@ class _EmptyState extends StatelessWidget { context.translate(I18n.contactsEmptyHint), style: TextStyle( fontSize: SizeUtils.getByScreen(small: 14, big: 13), - color: Theme.of(context).colorScheme.onSurface + color: Theme.of(context) + .colorScheme + .onSurface .withValues(alpha: 0.5), ), ), diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/edit_contact_screen.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/edit_contact_screen.dart index 65f9a84e..8eb6248c 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/edit_contact_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/edit_contact_screen.dart @@ -1,15 +1,18 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/edit_contact_form_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get_it/get_it.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import 'state/edit_contact_view_model.dart'; - class EditContactScreen extends ConsumerWidget { final String contactId; @@ -17,68 +20,168 @@ class EditContactScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final provider = editContactViewModelProvider(contactId); - final vm = ref.read(provider.notifier); - final state = ref.watch(provider); - - ref.listen(provider.select((s) => s.error), (_, next) { - if (next == null) return; - showTopSnackbar( - context, - message: context.translate(next.i18nKey), - type: MessageType.error, - ); - vm.clearError(); - }); - - final contact = state.contact; - - if (contact == null) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: Center(child: Text(context.translate(I18n.errorGeneric))), - ); - } + final userAsync = ref.watch(userInfoProvider); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( - child: Column( - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: SizeUtils.getByScreen(small: 22, big: 21), - vertical: SizeUtils.getByScreen(small: 10, big: 8), + child: userAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text(context.translate(I18n.errorGeneric)), + ), + data: (user) { + final contactsAsync = ref.watch(contactsProvider(user.id)); + return contactsAsync.when( + loading: () => + const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text(context.translate(I18n.errorGeneric)), ), - child: Stack( - children: [ - IconButton( - onPressed: () => GetIt.I().goBack(), - icon: const Icon(Icons.arrow_back), - ), - Center( - child: Text( - context.translate(I18n.editContact), - style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 28, big: 27), - ), - ), - ), - ], + data: (contacts) { + final contact = + contacts.where((c) => c.id == contactId).firstOrNull; + if (contact == null) { + return Center( + child: Text(context.translate(I18n.errorGeneric)), + ); + } + return _EditContactForm(userId: user.id, contact: contact); + }, + ); + }, + ), + ), + ); + } +} + +class _EditContactForm extends ConsumerStatefulWidget { + final String userId; + final ContactEntity contact; + + const _EditContactForm({required this.userId, required this.contact}); + + @override + ConsumerState<_EditContactForm> createState() => _EditContactFormState(); +} + +class _EditContactFormState extends ConsumerState<_EditContactForm> { + late final TextEditingController _nameController; + late final TextEditingController _phoneController; + late final String _initialIsoCode; + + @override + void initState() { + super.initState(); + final parsed = widget.contact.phone; + _nameController = TextEditingController(text: widget.contact.name); + _phoneController = TextEditingController( + text: parsed?.nationalNumber ?? widget.contact.rawPhone, + ); + _initialIsoCode = parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode; + + _nameController.addListener(_clearError); + _phoneController.addListener(_clearError); + + ref.listenManual(contactsControllerProvider, (prev, next) async { + if (prev == null || !prev.isLoading || next.isLoading) return; + final action = + ref.read(contactsControllerProvider.notifier).lastAction; + if (action != ContactsAction.update) return; + if (next.hasError) { + await next.showErrorOn(context); + return; + } + if (mounted) GetIt.I().goBack(); + }); + } + + @override + void dispose() { + _nameController.removeListener(_clearError); + _phoneController.removeListener(_clearError); + _nameController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + void _clearError() { + ref.read(editContactFormProvider(_initialIsoCode).notifier).clearError(); + } + + void _submit() { + final formNotifier = + ref.read(editContactFormProvider(_initialIsoCode).notifier); + final name = _nameController.text.trim(); + if (name.isEmpty) { + formNotifier.setLocalError(I18n.errorFirstNameRequired); + return; + } + + final isoCode = + ref.read(editContactFormProvider(_initialIsoCode)).isoCode; + final parsed = SfPhoneNumber.tryParse( + _phoneController.text, + defaultIsoCode: isoCode, + ); + if (parsed == null) { + formNotifier.setLocalError(I18n.errorMessagePhoneIsInvalid); + return; + } + + formNotifier.clearError(); + ref.read(contactsControllerProvider.notifier).updateContact( + userId: widget.userId, + contactId: widget.contact.id, + name: name, + phoneE164: parsed.e164, + ); + } + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + final formState = ref.watch(editContactFormProvider(_initialIsoCode)); + final isSubmitting = + ref.watch(contactsControllerProvider.select((s) => s.isLoading)); + + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 22, big: 21), + vertical: SizeUtils.getByScreen(small: 10, big: 8), + ), + child: Stack( + children: [ + IconButton( + onPressed: () => GetIt.I().goBack(), + icon: const Icon(Icons.arrow_back), ), - ), - SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)), - Expanded( - child: Container( - padding: EdgeInsets.symmetric( - horizontal: SizeUtils.getByScreen(small: 48, big: 47), - vertical: SizeUtils.getByScreen(small: 10, big: 8), + Center( + child: Text( + context.translate(I18n.editContact), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 28, big: 27), + ), ), - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( + ), + ], + ), + ), + SizedBox(height: SizeUtils.getByScreen(small: 20, big: 18)), + Expanded( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: SizeUtils.getByScreen(small: 48, big: 47), + vertical: SizeUtils.getByScreen(small: 10, big: 8), + ), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Center( @@ -90,7 +193,7 @@ class EditContactScreen extends ConsumerWidget { height: SizeUtils.getByScreen(small: 24, big: 22), ), CustomTextField( - controller: vm.nameController, + controller: _nameController, label: context.translate(I18n.name), ), SizedBox( @@ -102,10 +205,16 @@ class EditContactScreen extends ConsumerWidget { headerText: context.translate( I18n.selectYourCountry, ), - initialSelection: state.isoCode, + initialSelection: formState.isoCode, onChanged: (country) { final code = country.code; - if (code != null) vm.updateCountry(code); + if (code != null) { + ref + .read(editContactFormProvider( + _initialIsoCode) + .notifier) + .setIsoCode(code); + } }, width: 80, ), @@ -118,37 +227,38 @@ class EditContactScreen extends ConsumerWidget { ), Expanded( child: CustomTextField( - controller: vm.phoneController, + controller: _phoneController, keyboardType: TextInputType.number, ), ), ], ), - ], - ), - ), + if (formState.localError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + context.translate(formState.localError!), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, + ), + ), + ), + ], ), - SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), - PrimaryButton( - onPressed: state.isSubmitting - ? null - : () async { - final success = await vm.submit(); - if (success && context.mounted) { - GetIt.I().goBack(); - } - }, - text: context.translate(I18n.save), - color: context.sfColors.legacyPrimary, - ), - ], + ), ), - ), + SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), + PrimaryButton( + onPressed: isSubmitting ? null : _submit, + text: context.translate(I18n.save), + color: primaryColor, + ), + ], ), - ], + ), ), - ), + ], ); } } - diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.dart new file mode 100644 index 00000000..d1e2e99b --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:device_management/src/core/data/models/create_contact_request_dto.dart'; +import 'package:device_management/src/core/data/models/update_contact_request_dto.dart'; +import 'package:device_management/src/core/providers/contacts_repository_provider.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_tracking/sf_tracking.dart'; +import 'package:uuid/uuid.dart'; + +part 'contacts_controller.g.dart'; + +class ContactsMinReachedException implements Exception { + const ContactsMinReachedException(); +} + +class ContactsMaxReachedException implements Exception { + const ContactsMaxReachedException(); +} + +enum ContactsAction { create, update, delete } + +@riverpod +class ContactsController extends _$ContactsController { + static const _uuid = Uuid(); + + ContactsAction? _lastAction; + + ContactsAction? get lastAction => _lastAction; + + @override + FutureOr build() {} + + Future createContact({ + required String userId, + required String name, + required String phoneE164, + required int currentCount, + required int maxCount, + }) async { + _lastAction = ContactsAction.create; + state = const AsyncLoading(); + if (currentCount >= maxCount) { + state = AsyncError( + const ContactsMaxReachedException(), + StackTrace.current, + ); + return; + } + state = await AsyncValue.guard(() async { + await ref.read(contactsRepositoryProvider).createContact( + request: CreateContactRequestDto( + id: _uuid.v4(), + name: name, + phone: phoneE164, + userId: userId, + ), + ); + ref.invalidate(contactsProvider(userId)); + unawaited( + ref + .read(sfTrackingProvider) + .legacyContactsAdded(totalCount: currentCount + 1), + ); + }); + } + + Future updateContact({ + required String userId, + required String contactId, + required String name, + required String phoneE164, + }) async { + _lastAction = ContactsAction.update; + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref.read(contactsRepositoryProvider).updateContact( + request: UpdateContactRequestDto( + id: contactId, + name: name, + phone: phoneE164, + ), + ); + ref.invalidate(contactsProvider(userId)); + unawaited(ref.read(sfTrackingProvider).legacyContactsEdited()); + }); + } + + Future deleteContact({ + required String userId, + required String contactId, + required int currentCount, + }) async { + _lastAction = ContactsAction.delete; + state = const AsyncLoading(); + if (currentCount <= 1) { + state = AsyncError( + const ContactsMinReachedException(), + StackTrace.current, + ); + return; + } + state = await AsyncValue.guard(() async { + await ref + .read(contactsRepositoryProvider) + .deleteContact(contactId: contactId); + ref.invalidate(contactsProvider(userId)); + unawaited( + ref + .read(sfTrackingProvider) + .legacyContactsDeleted(totalCount: currentCount - 1), + ); + }); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.g.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.g.dart new file mode 100644 index 00000000..4c2f5226 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contacts_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ContactsController) +const contactsControllerProvider = ContactsControllerProvider._(); + +final class ContactsControllerProvider + extends $AsyncNotifierProvider { + const ContactsControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'contactsControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$contactsControllerHash(); + + @$internal + @override + ContactsController create() => ContactsController(); +} + +String _$contactsControllerHash() => + r'a304d9260a03626ce75b0cf5e62bf2663ee9feec'; + +abstract class _$ContactsController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart new file mode 100644 index 00000000..5b23bc6e --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.dart @@ -0,0 +1,16 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'contacts_editing_mode_provider.g.dart'; + +@riverpod +class ContactsEditingMode extends _$ContactsEditingMode { + @override + bool build() => false; + + void toggle() => state = !state; + + void set(bool value) { + if (value == state) return; + state = value; + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.g.dart new file mode 100644 index 00000000..0611795b --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_editing_mode_provider.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contacts_editing_mode_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ContactsEditingMode) +const contactsEditingModeProvider = ContactsEditingModeProvider._(); + +final class ContactsEditingModeProvider + extends $NotifierProvider { + const ContactsEditingModeProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'contactsEditingModeProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$contactsEditingModeHash(); + + @$internal + @override + ContactsEditingMode create() => ContactsEditingMode(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$contactsEditingModeHash() => + r'b06c06615bcd5e5f3f74e82f9e4695150df25123'; + +abstract class _$ContactsEditingMode extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.dart new file mode 100644 index 00000000..f0cd76a9 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:device_management/src/core/providers/contacts_repository_provider.dart'; +import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_shared/sf_shared.dart'; + +part 'contacts_provider.g.dart'; + +@riverpod +Future> contacts(Ref ref, String userId) async { + final repo = ref.read(contactsRepositoryProvider); + final contacts = await repo.getContacts(userId: userId); + + final device = ref.read(selectedDeviceProvider).value; + if (device != null) { + unawaited( + repo.syncContactsToDevice( + userId: userId, + deviceId: device.id, + contacts: contacts, + ), + ); + } + return contacts; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.g.dart new file mode 100644 index 00000000..7a9a2d71 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/contacts_provider.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contacts_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(contacts) +const contactsProvider = ContactsFamily._(); + +final class ContactsProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + const ContactsProvider._({ + required ContactsFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'contactsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$contactsHash(); + + @override + String toString() { + return r'contactsProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as String; + return contacts(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is ContactsProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$contactsHash() => r'59d591cbb75107782b89807213671eb9302f8133'; + +final class ContactsFamily extends $Family + with $FunctionalFamilyOverride>, String> { + const ContactsFamily._() + : super( + retry: null, + name: r'contactsProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ContactsProvider call(String userId) => + ContactsProvider._(argument: userId, from: this); + + @override + String toString() => r'contactsProvider'; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.dart new file mode 100644 index 00000000..3cf9f189 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.dart @@ -0,0 +1,46 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_shared/sf_shared.dart'; + +part 'edit_contact_form_provider.g.dart'; + +class EditContactFormState { + const EditContactFormState({required this.isoCode, this.localError}); + + final String isoCode; + final String? localError; + + EditContactFormState copyWith({ + String? isoCode, + String? localError, + bool clearError = false, + }) { + return EditContactFormState( + isoCode: isoCode ?? this.isoCode, + localError: clearError ? null : (localError ?? this.localError), + ); + } +} + +@riverpod +class EditContactForm extends _$EditContactForm { + @override + EditContactFormState build(String initialIsoCode) => EditContactFormState( + isoCode: initialIsoCode.isNotEmpty + ? initialIsoCode + : SfPhoneNumber.defaultIsoCode, + ); + + void setIsoCode(String code) { + if (code == state.isoCode) return; + state = state.copyWith(isoCode: code); + } + + void setLocalError(String key) { + state = state.copyWith(localError: key); + } + + void clearError() { + if (state.localError == null) return; + state = state.copyWith(clearError: true); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.g.dart new file mode 100644 index 00000000..a7e4f24a --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/edit_contact_form_provider.g.dart @@ -0,0 +1,108 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'edit_contact_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(EditContactForm) +const editContactFormProvider = EditContactFormFamily._(); + +final class EditContactFormProvider + extends $NotifierProvider { + const EditContactFormProvider._({ + required EditContactFormFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'editContactFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$editContactFormHash(); + + @override + String toString() { + return r'editContactFormProvider' + '' + '($argument)'; + } + + @$internal + @override + EditContactForm create() => EditContactForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(EditContactFormState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is EditContactFormProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$editContactFormHash() => r'8a6ee15611761bd53cf1d335bd16ed5443d7400c'; + +final class EditContactFormFamily extends $Family + with + $ClassFamilyOverride< + EditContactForm, + EditContactFormState, + EditContactFormState, + EditContactFormState, + String + > { + const EditContactFormFamily._() + : super( + retry: null, + name: r'editContactFormProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + EditContactFormProvider call(String initialIsoCode) => + EditContactFormProvider._(argument: initialIsoCode, from: this); + + @override + String toString() => r'editContactFormProvider'; +} + +abstract class _$EditContactForm extends $Notifier { + late final _$args = ref.$arg as String; + String get initialIsoCode => _$args; + + EditContactFormState build(String initialIsoCode); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + EditContactFormState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.dart new file mode 100644 index 00000000..dcb1e649 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.dart @@ -0,0 +1,44 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'new_contact_form_provider.g.dart'; + +class NewContactFormState { + const NewContactFormState({ + this.isoCode = 'ES', + this.localError, + }); + + final String isoCode; + final String? localError; + + NewContactFormState copyWith({ + String? isoCode, + String? localError, + bool clearError = false, + }) { + return NewContactFormState( + isoCode: isoCode ?? this.isoCode, + localError: clearError ? null : (localError ?? this.localError), + ); + } +} + +@riverpod +class NewContactForm extends _$NewContactForm { + @override + NewContactFormState build() => const NewContactFormState(); + + void setIsoCode(String code) { + if (code == state.isoCode) return; + state = state.copyWith(isoCode: code); + } + + void setLocalError(String key) { + state = state.copyWith(localError: key); + } + + void clearError() { + if (state.localError == null) return; + state = state.copyWith(clearError: true); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.g.dart new file mode 100644 index 00000000..6cf81153 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/providers/new_contact_form_provider.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'new_contact_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(NewContactForm) +const newContactFormProvider = NewContactFormProvider._(); + +final class NewContactFormProvider + extends $NotifierProvider { + const NewContactFormProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'newContactFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$newContactFormHash(); + + @$internal + @override + NewContactForm create() => NewContactForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(NewContactFormState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$newContactFormHash() => r'fc9f3c830229b36ebafbe10fa788e8b02294c215'; + +abstract class _$NewContactForm extends $Notifier { + NewContactFormState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + NewContactFormState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart deleted file mode 100644 index 7f8c06fd..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -import '../../../../core/domain/repositories/contacts_repository.dart'; -import '../../../../core/providers/contacts_repository_provider.dart'; -import '../../domain/entities/contact_entity.dart'; -import '../../domain/entities/contact_error.dart'; -import 'contacts_view_state.dart'; - -final contactsViewModelProvider = - NotifierProvider.autoDispose( - ContactsViewModel.new, - ); - -class ContactsViewModel extends Notifier { - late final ContactsRepository _contactsRepository; - late final SfTrackingRepository _tracking; - - @override - ContactsViewState build() { - _contactsRepository = ref.read(contactsRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - final capabilities = ref.read(selectedDeviceProvider).value?.capabilities; - final maxContacts = capabilities?.contacts?.maxForType('white') ?? 10; - - _load(); - return ContactsViewState(maxContacts: maxContacts); - } - - void toggleIsEditing() { - state = state.copyWith(isEditing: !state.isEditing); - } - - void clearError() { - if (state.error != null) state = state.copyWith(error: null); - } - - Future refresh() async { - try { - final user = await ref.read(userInfoProvider.future); - if (!ref.mounted) return; - await _reload(user.id); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith(error: ContactError.network); - } - } - - Future deleteContact(ContactEntity contact) async { - if (state.isSubmitting) return false; - - if (state.contacts.length <= 1) { - state = state.copyWith(error: ContactError.minReached); - return false; - } - - state = state.copyWith(isSubmitting: true, error: null); - - try { - await _contactsRepository.deleteContact(contactId: contact.id); - if (!ref.mounted) return false; - - unawaited( - _tracking.legacyContactsDeleted( - totalCount: (state.contacts.length - 1).clamp(0, 999), - ), - ); - - final user = await ref.read(userInfoProvider.future); - if (!ref.mounted) return false; - - await _reload(user.id); - return true; - } catch (_) { - if (!ref.mounted) return false; - state = state.copyWith(isSubmitting: false, error: ContactError.network); - return false; - } - } - - Future _load() async { - try { - final user = await ref.read(userInfoProvider.future); - if (!ref.mounted) return; - await _reload(user.id); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith(isLoading: false, error: ContactError.network); - } - } - - Future _reload(String userId) async { - final contacts = await _contactsRepository.getContacts(userId: userId); - if (!ref.mounted) return; - - state = state.copyWith( - contacts: contacts, - isLoading: false, - isSubmitting: false, - ); - - unawaited(_syncToDevice(userId, contacts)); - } - - Future _syncToDevice( - String userId, - List contacts, - ) async { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - try { - await _contactsRepository.syncContactsToDevice( - userId: userId, - deviceId: device.id, - contacts: contacts, - ); - } catch (_) { - // Non-blocking: CRUD already committed server-side. - } - } -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.dart deleted file mode 100644 index 21ee1f7d..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../domain/entities/contact_entity.dart'; -import '../../domain/entities/contact_error.dart'; - -part 'contacts_view_state.freezed.dart'; - -@freezed -abstract class ContactsViewState with _$ContactsViewState { - const factory ContactsViewState({ - @Default([]) List contacts, - @Default(true) bool isLoading, - @Default(false) bool isSubmitting, - @Default(false) bool isEditing, - @Default(10) int maxContacts, - ContactError? error, - }) = _ContactsViewState; -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.freezed.dart deleted file mode 100644 index ea93e4cf..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_state.freezed.dart +++ /dev/null @@ -1,292 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'contacts_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$ContactsViewState { - - List get contacts; bool get isLoading; bool get isSubmitting; bool get isEditing; int get maxContacts; ContactError? get error; -/// Create a copy of ContactsViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$ContactsViewStateCopyWith get copyWith => _$ContactsViewStateCopyWithImpl(this as ContactsViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ContactsViewState&&const DeepCollectionEquality().equals(other.contacts, contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.maxContacts, maxContacts) || other.maxContacts == maxContacts)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(contacts),isLoading,isSubmitting,isEditing,maxContacts,error); - -@override -String toString() { - return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, maxContacts: $maxContacts, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class $ContactsViewStateCopyWith<$Res> { - factory $ContactsViewStateCopyWith(ContactsViewState value, $Res Function(ContactsViewState) _then) = _$ContactsViewStateCopyWithImpl; -@useResult -$Res call({ - List contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error -}); - - - - -} -/// @nodoc -class _$ContactsViewStateCopyWithImpl<$Res> - implements $ContactsViewStateCopyWith<$Res> { - _$ContactsViewStateCopyWithImpl(this._self, this._then); - - final ContactsViewState _self; - final $Res Function(ContactsViewState) _then; - -/// Create a copy of ContactsViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? contacts = null,Object? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? maxContacts = null,Object? error = freezed,}) { - return _then(_self.copyWith( -contacts: null == contacts ? _self.contacts : contacts // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable -as bool,maxContacts: null == maxContacts ? _self.maxContacts : maxContacts // ignore: cast_nullable_to_non_nullable -as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [ContactsViewState]. -extension ContactsViewStatePatterns on ContactsViewState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _ContactsViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _ContactsViewState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _ContactsViewState value) $default,){ -final _that = this; -switch (_that) { -case _ContactsViewState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ContactsViewState value)? $default,){ -final _that = this; -switch (_that) { -case _ContactsViewState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( List contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _ContactsViewState() when $default != null: -return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_that.error);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( List contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error) $default,) {final _that = this; -switch (_that) { -case _ContactsViewState(): -return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_that.error);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error)? $default,) {final _that = this; -switch (_that) { -case _ContactsViewState() when $default != null: -return $default(_that.contacts,_that.isLoading,_that.isSubmitting,_that.isEditing,_that.maxContacts,_that.error);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _ContactsViewState implements ContactsViewState { - const _ContactsViewState({final List contacts = const [], this.isLoading = true, this.isSubmitting = false, this.isEditing = false, this.maxContacts = 10, this.error}): _contacts = contacts; - - - final List _contacts; -@override@JsonKey() List get contacts { - if (_contacts is EqualUnmodifiableListView) return _contacts; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_contacts); -} - -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isSubmitting; -@override@JsonKey() final bool isEditing; -@override@JsonKey() final int maxContacts; -@override final ContactError? error; - -/// Create a copy of ContactsViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$ContactsViewStateCopyWith<_ContactsViewState> get copyWith => __$ContactsViewStateCopyWithImpl<_ContactsViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContactsViewState&&const DeepCollectionEquality().equals(other._contacts, _contacts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.isEditing, isEditing) || other.isEditing == isEditing)&&(identical(other.maxContacts, maxContacts) || other.maxContacts == maxContacts)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_contacts),isLoading,isSubmitting,isEditing,maxContacts,error); - -@override -String toString() { - return 'ContactsViewState(contacts: $contacts, isLoading: $isLoading, isSubmitting: $isSubmitting, isEditing: $isEditing, maxContacts: $maxContacts, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class _$ContactsViewStateCopyWith<$Res> implements $ContactsViewStateCopyWith<$Res> { - factory _$ContactsViewStateCopyWith(_ContactsViewState value, $Res Function(_ContactsViewState) _then) = __$ContactsViewStateCopyWithImpl; -@override @useResult -$Res call({ - List contacts, bool isLoading, bool isSubmitting, bool isEditing, int maxContacts, ContactError? error -}); - - - - -} -/// @nodoc -class __$ContactsViewStateCopyWithImpl<$Res> - implements _$ContactsViewStateCopyWith<$Res> { - __$ContactsViewStateCopyWithImpl(this._self, this._then); - - final _ContactsViewState _self; - final $Res Function(_ContactsViewState) _then; - -/// Create a copy of ContactsViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? contacts = null,Object? isLoading = null,Object? isSubmitting = null,Object? isEditing = null,Object? maxContacts = null,Object? error = freezed,}) { - return _then(_ContactsViewState( -contacts: null == contacts ? _self._contacts : contacts // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,isEditing: null == isEditing ? _self.isEditing : isEditing // ignore: cast_nullable_to_non_nullable -as bool,maxContacts: null == maxContacts ? _self.maxContacts : maxContacts // ignore: cast_nullable_to_non_nullable -as int,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_model.dart deleted file mode 100644 index 5e6fb0b6..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_model.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -import '../../../../core/data/models/update_contact_request_dto.dart'; -import '../../../../core/domain/repositories/contacts_repository.dart'; -import '../../../../core/providers/contacts_repository_provider.dart'; -import '../../domain/entities/contact_error.dart'; -import 'contacts_view_model.dart'; -import 'edit_contact_view_state.dart'; - -final editContactViewModelProvider = NotifierProvider.autoDispose - .family( - EditContactViewModel.new, - ); - -class EditContactViewModel extends Notifier { - EditContactViewModel(this._contactId); - - final String _contactId; - - late final TextEditingController nameController; - late final TextEditingController phoneController; - - late final ContactsRepository _contactsRepository; - late final SfTrackingRepository _tracking; - - @override - EditContactViewState build() { - _contactsRepository = ref.read(contactsRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - final contact = ref - .read(contactsViewModelProvider) - .contacts - .where((c) => c.id == _contactId) - .firstOrNull; - - final parsed = contact?.phone; - - nameController = TextEditingController(text: contact?.name ?? ''); - phoneController = TextEditingController( - text: parsed?.nationalNumber ?? contact?.rawPhone ?? '', - ); - - ref.onDispose(() { - nameController.dispose(); - phoneController.dispose(); - }); - - return EditContactViewState( - contactId: _contactId, - isoCode: parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode, - contact: contact, - ); - } - - void updateCountry(String isoCode) { - if (isoCode == state.isoCode) return; - state = state.copyWith(isoCode: isoCode); - } - - void clearError() { - if (state.error != null) state = state.copyWith(error: null); - } - - Future submit() async { - final contact = state.contact; - if (contact == null || state.isSubmitting) return false; - - final name = nameController.text.trim(); - if (name.isEmpty) { - state = state.copyWith(error: ContactError.nameRequired); - return false; - } - - final phone = SfPhoneNumber.tryParse( - phoneController.text, - defaultIsoCode: state.isoCode, - ); - if (phone == null) { - state = state.copyWith(error: ContactError.phoneInvalid); - return false; - } - - state = state.copyWith(isSubmitting: true, error: null); - - try { - await _contactsRepository.updateContact( - request: UpdateContactRequestDto( - id: contact.id, - name: name, - phone: phone.e164, - ), - ); - if (!ref.mounted) return false; - - unawaited(_tracking.legacyContactsEdited()); - - await ref.read(contactsViewModelProvider.notifier).refresh(); - return true; - } catch (_) { - if (!ref.mounted) return false; - state = state.copyWith(isSubmitting: false, error: ContactError.network); - return false; - } - } -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.dart deleted file mode 100644 index cfaf7a1e..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../domain/entities/contact_entity.dart'; -import '../../domain/entities/contact_error.dart'; - -part 'edit_contact_view_state.freezed.dart'; - -@freezed -abstract class EditContactViewState with _$EditContactViewState { - const factory EditContactViewState({ - required String contactId, - required String isoCode, - ContactEntity? contact, - @Default(false) bool isSubmitting, - ContactError? error, - }) = _EditContactViewState; -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.freezed.dart deleted file mode 100644 index 233f424f..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/edit_contact_view_state.freezed.dart +++ /dev/null @@ -1,307 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'edit_contact_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$EditContactViewState { - - String get contactId; String get isoCode; ContactEntity? get contact; bool get isSubmitting; ContactError? get error; -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$EditContactViewStateCopyWith get copyWith => _$EditContactViewStateCopyWithImpl(this as EditContactViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is EditContactViewState&&(identical(other.contactId, contactId) || other.contactId == contactId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.contact, contact) || other.contact == contact)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,contactId,isoCode,contact,isSubmitting,error); - -@override -String toString() { - return 'EditContactViewState(contactId: $contactId, isoCode: $isoCode, contact: $contact, isSubmitting: $isSubmitting, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class $EditContactViewStateCopyWith<$Res> { - factory $EditContactViewStateCopyWith(EditContactViewState value, $Res Function(EditContactViewState) _then) = _$EditContactViewStateCopyWithImpl; -@useResult -$Res call({ - String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error -}); - - -$ContactEntityCopyWith<$Res>? get contact; - -} -/// @nodoc -class _$EditContactViewStateCopyWithImpl<$Res> - implements $EditContactViewStateCopyWith<$Res> { - _$EditContactViewStateCopyWithImpl(this._self, this._then); - - final EditContactViewState _self; - final $Res Function(EditContactViewState) _then; - -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? contactId = null,Object? isoCode = null,Object? contact = freezed,Object? isSubmitting = null,Object? error = freezed,}) { - return _then(_self.copyWith( -contactId: null == contactId ? _self.contactId : contactId // ignore: cast_nullable_to_non_nullable -as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,contact: freezed == contact ? _self.contact : contact // ignore: cast_nullable_to_non_nullable -as ContactEntity?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ContactEntityCopyWith<$Res>? get contact { - if (_self.contact == null) { - return null; - } - - return $ContactEntityCopyWith<$Res>(_self.contact!, (value) { - return _then(_self.copyWith(contact: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [EditContactViewState]. -extension EditContactViewStatePatterns on EditContactViewState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _EditContactViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _EditContactViewState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _EditContactViewState value) $default,){ -final _that = this; -switch (_that) { -case _EditContactViewState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _EditContactViewState value)? $default,){ -final _that = this; -switch (_that) { -case _EditContactViewState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _EditContactViewState() when $default != null: -return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error) $default,) {final _that = this; -switch (_that) { -case _EditContactViewState(): -return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error)? $default,) {final _that = this; -switch (_that) { -case _EditContactViewState() when $default != null: -return $default(_that.contactId,_that.isoCode,_that.contact,_that.isSubmitting,_that.error);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _EditContactViewState implements EditContactViewState { - const _EditContactViewState({required this.contactId, required this.isoCode, this.contact, this.isSubmitting = false, this.error}); - - -@override final String contactId; -@override final String isoCode; -@override final ContactEntity? contact; -@override@JsonKey() final bool isSubmitting; -@override final ContactError? error; - -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$EditContactViewStateCopyWith<_EditContactViewState> get copyWith => __$EditContactViewStateCopyWithImpl<_EditContactViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditContactViewState&&(identical(other.contactId, contactId) || other.contactId == contactId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.contact, contact) || other.contact == contact)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,contactId,isoCode,contact,isSubmitting,error); - -@override -String toString() { - return 'EditContactViewState(contactId: $contactId, isoCode: $isoCode, contact: $contact, isSubmitting: $isSubmitting, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class _$EditContactViewStateCopyWith<$Res> implements $EditContactViewStateCopyWith<$Res> { - factory _$EditContactViewStateCopyWith(_EditContactViewState value, $Res Function(_EditContactViewState) _then) = __$EditContactViewStateCopyWithImpl; -@override @useResult -$Res call({ - String contactId, String isoCode, ContactEntity? contact, bool isSubmitting, ContactError? error -}); - - -@override $ContactEntityCopyWith<$Res>? get contact; - -} -/// @nodoc -class __$EditContactViewStateCopyWithImpl<$Res> - implements _$EditContactViewStateCopyWith<$Res> { - __$EditContactViewStateCopyWithImpl(this._self, this._then); - - final _EditContactViewState _self; - final $Res Function(_EditContactViewState) _then; - -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? contactId = null,Object? isoCode = null,Object? contact = freezed,Object? isSubmitting = null,Object? error = freezed,}) { - return _then(_EditContactViewState( -contactId: null == contactId ? _self.contactId : contactId // ignore: cast_nullable_to_non_nullable -as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,contact: freezed == contact ? _self.contact : contact // ignore: cast_nullable_to_non_nullable -as ContactEntity?,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} - -/// Create a copy of EditContactViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ContactEntityCopyWith<$Res>? get contact { - if (_self.contact == null) { - return null; - } - - return $ContactEntityCopyWith<$Res>(_self.contact!, (value) { - return _then(_self.copyWith(contact: value)); - }); -} -} - -// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_model.dart deleted file mode 100644 index 35a121e0..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_model.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; -import 'package:uuid/uuid.dart'; - -import '../../../../core/data/models/create_contact_request_dto.dart'; -import '../../../../core/domain/repositories/contacts_repository.dart'; -import '../../../../core/providers/contacts_repository_provider.dart'; -import '../../domain/entities/contact_error.dart'; -import 'contacts_view_model.dart'; -import 'new_contact_view_state.dart'; - -final newContactViewModelProvider = - NotifierProvider.autoDispose( - NewContactViewModel.new, - ); - -class NewContactViewModel extends Notifier { - static const _uuid = Uuid(); - - late final TextEditingController nameController; - late final TextEditingController phoneController; - - late final ContactsRepository _contactsRepository; - late final SfTrackingRepository _tracking; - - @override - NewContactViewState build() { - _contactsRepository = ref.read(contactsRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - nameController = TextEditingController(); - phoneController = TextEditingController(); - - ref.onDispose(() { - nameController.dispose(); - phoneController.dispose(); - }); - - return const NewContactViewState(); - } - - void updateCountry(String isoCode) { - if (isoCode == state.isoCode) return; - state = state.copyWith(isoCode: isoCode); - } - - void clearError() { - if (state.error != null) state = state.copyWith(error: null); - } - - Future pickContactFromDevice() async { - final response = await ref - .read(deviceContactPickerProvider) - .pick(hintIsoCode: state.isoCode); - - if (response.outcome == - DeviceContactPickOutcome.permissionPermanentlyDenied) { - state = state.copyWith(error: ContactError.contactsPermissionBlocked); - return; - } - - final data = response.data; - if (data == null) return; - - final parsed = data.parsedPhone; - if (parsed != null) { - state = state.copyWith(isoCode: parsed.isoCode, error: null); - phoneController.text = parsed.nationalNumber; - } else { - phoneController.text = data.rawNumber; - } - - if (nameController.text.trim().isEmpty) { - nameController.text = data.displayName; - } - } - - Future openSystemSettings() => - ref.read(deviceContactPickerProvider).openSystemSettings(); - - Future submit() async { - if (state.isSubmitting) return false; - - final listState = ref.read(contactsViewModelProvider); - if (listState.contacts.length >= listState.maxContacts) { - state = state.copyWith(error: ContactError.maxReached); - return false; - } - - final name = nameController.text.trim(); - if (name.isEmpty) { - state = state.copyWith(error: ContactError.nameRequired); - return false; - } - - final phone = SfPhoneNumber.tryParse( - phoneController.text, - defaultIsoCode: state.isoCode, - ); - if (phone == null) { - state = state.copyWith(error: ContactError.phoneInvalid); - return false; - } - - state = state.copyWith(isSubmitting: true, error: null); - - try { - final user = await ref.read(userInfoProvider.future); - if (!ref.mounted) return false; - - await _contactsRepository.createContact( - request: CreateContactRequestDto( - id: _uuid.v4(), - name: name, - phone: phone.e164, - userId: user.id, - ), - ); - if (!ref.mounted) return false; - - unawaited( - _tracking.legacyContactsAdded( - totalCount: listState.contacts.length + 1, - ), - ); - - await ref.read(contactsViewModelProvider.notifier).refresh(); - return true; - } catch (_) { - if (!ref.mounted) return false; - state = state.copyWith(isSubmitting: false, error: ContactError.network); - return false; - } - } -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.dart deleted file mode 100644 index 9a4882a9..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../domain/entities/contact_error.dart'; - -part 'new_contact_view_state.freezed.dart'; - -@freezed -abstract class NewContactViewState with _$NewContactViewState { - const factory NewContactViewState({ - @Default('ES') String isoCode, - @Default(false) bool isSubmitting, - ContactError? error, - }) = _NewContactViewState; -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.freezed.dart deleted file mode 100644 index 9f8d8c72..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/new_contact_view_state.freezed.dart +++ /dev/null @@ -1,277 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'new_contact_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$NewContactViewState { - - String get isoCode; bool get isSubmitting; ContactError? get error; -/// Create a copy of NewContactViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$NewContactViewStateCopyWith get copyWith => _$NewContactViewStateCopyWithImpl(this as NewContactViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is NewContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isoCode,isSubmitting,error); - -@override -String toString() { - return 'NewContactViewState(isoCode: $isoCode, isSubmitting: $isSubmitting, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class $NewContactViewStateCopyWith<$Res> { - factory $NewContactViewStateCopyWith(NewContactViewState value, $Res Function(NewContactViewState) _then) = _$NewContactViewStateCopyWithImpl; -@useResult -$Res call({ - String isoCode, bool isSubmitting, ContactError? error -}); - - - - -} -/// @nodoc -class _$NewContactViewStateCopyWithImpl<$Res> - implements $NewContactViewStateCopyWith<$Res> { - _$NewContactViewStateCopyWithImpl(this._self, this._then); - - final NewContactViewState _self; - final $Res Function(NewContactViewState) _then; - -/// Create a copy of NewContactViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isoCode = null,Object? isSubmitting = null,Object? error = freezed,}) { - return _then(_self.copyWith( -isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [NewContactViewState]. -extension NewContactViewStatePatterns on NewContactViewState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _NewContactViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _NewContactViewState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _NewContactViewState value) $default,){ -final _that = this; -switch (_that) { -case _NewContactViewState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _NewContactViewState value)? $default,){ -final _that = this; -switch (_that) { -case _NewContactViewState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String isoCode, bool isSubmitting, ContactError? error)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _NewContactViewState() when $default != null: -return $default(_that.isoCode,_that.isSubmitting,_that.error);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String isoCode, bool isSubmitting, ContactError? error) $default,) {final _that = this; -switch (_that) { -case _NewContactViewState(): -return $default(_that.isoCode,_that.isSubmitting,_that.error);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String isoCode, bool isSubmitting, ContactError? error)? $default,) {final _that = this; -switch (_that) { -case _NewContactViewState() when $default != null: -return $default(_that.isoCode,_that.isSubmitting,_that.error);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _NewContactViewState implements NewContactViewState { - const _NewContactViewState({this.isoCode = 'ES', this.isSubmitting = false, this.error}); - - -@override@JsonKey() final String isoCode; -@override@JsonKey() final bool isSubmitting; -@override final ContactError? error; - -/// Create a copy of NewContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$NewContactViewStateCopyWith<_NewContactViewState> get copyWith => __$NewContactViewStateCopyWithImpl<_NewContactViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _NewContactViewState&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.error, error) || other.error == error)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isoCode,isSubmitting,error); - -@override -String toString() { - return 'NewContactViewState(isoCode: $isoCode, isSubmitting: $isSubmitting, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class _$NewContactViewStateCopyWith<$Res> implements $NewContactViewStateCopyWith<$Res> { - factory _$NewContactViewStateCopyWith(_NewContactViewState value, $Res Function(_NewContactViewState) _then) = __$NewContactViewStateCopyWithImpl; -@override @useResult -$Res call({ - String isoCode, bool isSubmitting, ContactError? error -}); - - - - -} -/// @nodoc -class __$NewContactViewStateCopyWithImpl<$Res> - implements _$NewContactViewStateCopyWith<$Res> { - __$NewContactViewStateCopyWithImpl(this._self, this._then); - - final _NewContactViewState _self; - final $Res Function(_NewContactViewState) _then; - -/// Create a copy of NewContactViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isoCode = null,Object? isSubmitting = null,Object? error = freezed,}) { - return _then(_NewContactViewState( -isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContactError?, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/confirm_delete_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/confirm_delete_dialog.dart deleted file mode 100644 index 5fd45364..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/confirm_delete_dialog.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_localizations/sf_localizations.dart'; -import 'package:utils/utils.dart'; - -import '../../domain/entities/contact_entity.dart'; -import '../state/contacts_view_model.dart'; -import 'package:legacy_theme/legacy_theme.dart'; - -class ConfirmDeleteDialog extends ConsumerWidget { - final ContactEntity contact; - - const ConfirmDeleteDialog({super.key, required this.contact}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - - return Container( - padding: EdgeInsets.symmetric( - horizontal: SizeUtils.getByScreen(small: 32, big: 30), - vertical: SizeUtils.getByScreen(small: 30, big: 28), - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular( - SizeUtils.getByScreen(small: 12, big: 10), - ), - ), - width: SizeUtils.getByScreen(small: 360, big: 350), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.translate(I18n.deleteContactMessage), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 19, big: 18), - ), - ), - SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: PrimaryButton( - onPressed: () => Navigator.pop(context), - text: context.translate(I18n.cancel), - color: context.sfColors.legacyPrimary, - height: SizeUtils.getByScreen(small: 38, big: 36), - radius: SizeUtils.getByScreen(small: 32, big: 34), - ), - ), - SizedBox(width: SizeUtils.getByScreen(small: 4, big: 16)), - Expanded( - child: PrimaryButton( - onPressed: () async { - final vm = ref.read(contactsViewModelProvider.notifier); - await vm.deleteContact(contact); - if (context.mounted) Navigator.pop(context); - }, - text: context.translate(I18n.delete), - color: Theme.of(context).colorScheme.error, - height: SizeUtils.getByScreen(small: 38, big: 36), - radius: SizeUtils.getByScreen(small: 32, big: 34), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/contact_card.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/contact_card.dart index 17beaa8e..a1f38b5e 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/contact_card.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/contact_card.dart @@ -1,27 +1,26 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:device_management/src/features/contacts/domain/entities/contact_entity.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:navigation/navigation.dart'; import 'package:utils/utils.dart'; -import '../../domain/entities/contact_entity.dart'; -import 'confirm_delete_dialog.dart'; - class ContactCard extends ConsumerWidget { final ContactEntity contact; final bool isEditing; + final VoidCallback onDelete; const ContactCard({ super.key, required this.contact, required this.isEditing, + required this.onDelete, }); @override Widget build(BuildContext context, WidgetRef ref) { - return Container( padding: EdgeInsets.symmetric( horizontal: SizeUtils.getByScreen(small: 22, big: 21), @@ -73,14 +72,10 @@ class ContactCard extends ConsumerWidget { DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.error, - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), ), child: IconButton( - onPressed: () => showDialog( - context: context, - builder: (_) => - Dialog(child: ConfirmDeleteDialog(contact: contact)), - ), + onPressed: onDelete, icon: const Icon(Icons.close, color: Colors.white), ), ), @@ -88,7 +83,7 @@ class ContactCard extends ConsumerWidget { DecoratedBox( decoration: BoxDecoration( color: context.sfColors.legacyPrimary, - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), ), child: IconButton( onPressed: () { diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/new_contact_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/new_contact_dialog.dart index 903efd23..9030ce57 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/new_contact_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/widgets/new_contact_dialog.dart @@ -1,43 +1,121 @@ import 'package:design_system/design_system.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/new_contact_form_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import '../../domain/entities/contact_error.dart'; -import '../state/new_contact_view_model.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +class NewContactDialog extends ConsumerStatefulWidget { + final String userId; + final int currentCount; + final int maxContacts; -class NewContactDialog extends ConsumerWidget { - const NewContactDialog({super.key}); + const NewContactDialog({ + super.key, + required this.userId, + required this.currentCount, + required this.maxContacts, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(newContactViewModelProvider.notifier); - final isoCode = ref.watch( - newContactViewModelProvider.select((s) => s.isoCode), - ); - final isSubmitting = ref.watch( - newContactViewModelProvider.select((s) => s.isSubmitting), - ); + ConsumerState createState() => _NewContactDialogState(); +} - ref.listen(newContactViewModelProvider.select((s) => s.error), (_, next) { - if (next == null) return; - if (next == ContactError.contactsPermissionBlocked) { - showContactsPermissionDialog( - context, - onOpenSettings: vm.openSystemSettings, +class _NewContactDialogState extends ConsumerState { + final _nameController = TextEditingController(); + final _phoneController = TextEditingController(); + + @override + void initState() { + super.initState(); + _nameController.addListener(_clearErrorIfAny); + _phoneController.addListener(_clearErrorIfAny); + } + + @override + void dispose() { + _nameController.removeListener(_clearErrorIfAny); + _phoneController.removeListener(_clearErrorIfAny); + _nameController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + void _clearErrorIfAny() { + ref.read(newContactFormProvider.notifier).clearError(); + } + + Future _pickContact() async { + final isoCode = ref.read(newContactFormProvider).isoCode; + final response = await ref + .read(deviceContactPickerProvider) + .pick(hintIsoCode: isoCode); + + if (response.outcome == + DeviceContactPickOutcome.permissionPermanentlyDenied) { + if (!mounted) return; + showContactsPermissionDialog( + context, + onOpenSettings: + ref.read(deviceContactPickerProvider).openSystemSettings, + ); + return; + } + + final data = response.data; + if (data == null) return; + + final parsed = data.parsedPhone; + if (parsed != null) { + ref.read(newContactFormProvider.notifier).setIsoCode(parsed.isoCode); + _phoneController.text = parsed.nationalNumber; + } else { + _phoneController.text = data.rawNumber; + } + if (_nameController.text.trim().isEmpty) { + _nameController.text = data.displayName; + } + } + + void _submit() { + final formNotifier = ref.read(newContactFormProvider.notifier); + final name = _nameController.text.trim(); + if (name.isEmpty) { + formNotifier.setLocalError(I18n.errorFirstNameRequired); + return; + } + + final isoCode = ref.read(newContactFormProvider).isoCode; + final parsed = SfPhoneNumber.tryParse( + _phoneController.text, + defaultIsoCode: isoCode, + ); + if (parsed == null) { + formNotifier.setLocalError(I18n.errorMessagePhoneIsInvalid); + return; + } + + formNotifier.clearError(); + Navigator.of(context).pop(); + ref.read(contactsControllerProvider.notifier).createContact( + userId: widget.userId, + name: name, + phoneE164: parsed.e164, + currentCount: widget.currentCount, + maxCount: widget.maxContacts, ); - } else { - showTopSnackbar( - context, - message: context.translate(next.i18nKey), - type: MessageType.error, - ); - } - vm.clearError(); - }); + } + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + final formState = ref.watch(newContactFormProvider); + final isSubmitting = + ref.watch(contactsControllerProvider.select((s) => s.isLoading)); return Container( padding: EdgeInsets.symmetric( @@ -61,17 +139,14 @@ class NewContactDialog extends ConsumerWidget { alignment: Alignment.topRight, child: IconButton( onPressed: () => Navigator.pop(context), - icon: Icon( - Icons.close, - color: context.sfColors.legacyPrimary, - ), + icon: Icon(Icons.close, color: primaryColor), ), ), Center( child: Text( context.translate(I18n.newContact).toUpperCase(), style: TextStyle( - color: context.sfColors.legacyPrimary, + color: primaryColor, fontWeight: FontWeight.w500, ), ), @@ -85,7 +160,7 @@ class NewContactDialog extends ConsumerWidget { SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)), CustomTextField( hint: context.translate(I18n.name), - controller: vm.nameController, + controller: _nameController, ), SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)), Row( @@ -93,16 +168,20 @@ class NewContactDialog extends ConsumerWidget { children: [ CountryPrefixPicker( headerText: context.translate(I18n.selectYourCountry), - initialSelection: isoCode, + initialSelection: formState.isoCode, onChanged: (country) { final code = country.code; - if (code != null) vm.updateCountry(code); + if (code != null) { + ref + .read(newContactFormProvider.notifier) + .setIsoCode(code); + } }, width: 80, ), Expanded( child: CustomTextField( - controller: vm.phoneController, + controller: _phoneController, hint: context.translate(I18n.phoneNumber), keyboardType: TextInputType.phone, ), @@ -110,10 +189,10 @@ class NewContactDialog extends ConsumerWidget { DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, - color: context.sfColors.legacyPrimary, + color: primaryColor, ), child: IconButton( - onPressed: vm.pickContactFromDevice, + onPressed: _pickContact, icon: Icon( SFIcons.contactsCircle, color: Colors.white, @@ -123,6 +202,17 @@ class NewContactDialog extends ConsumerWidget { ), ], ), + if (formState.localError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + context.translate(formState.localError!), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, + ), + ), + ), SizedBox(height: SizeUtils.getByScreen(small: 14, big: 12)), Padding( padding: EdgeInsets.symmetric( @@ -130,14 +220,9 @@ class NewContactDialog extends ConsumerWidget { vertical: SizeUtils.getByScreen(small: 10, big: 8), ), child: PrimaryButton( - onPressed: isSubmitting - ? null - : () async { - final success = await vm.submit(); - if (success && context.mounted) Navigator.pop(context); - }, + onPressed: isSubmitting ? null : _submit, text: context.translate(I18n.save), - color: context.sfColors.legacyPrimary, + color: primaryColor, ), ), ], @@ -146,4 +231,3 @@ class NewContactDialog extends ConsumerWidget { ); } } - diff --git a/modules/legacy/modules/device_management/test/features/contacts/contacts_controller_test.dart b/modules/legacy/modules/device_management/test/features/contacts/contacts_controller_test.dart new file mode 100644 index 00000000..952a32ae --- /dev/null +++ b/modules/legacy/modules/device_management/test/features/contacts/contacts_controller_test.dart @@ -0,0 +1,173 @@ +import 'package:device_management/src/core/data/models/create_contact_request_dto.dart'; +import 'package:device_management/src/core/data/models/update_contact_request_dto.dart'; +import 'package:device_management/src/core/domain/repositories/contacts_repository.dart'; +import 'package:device_management/src/core/providers/contacts_repository_provider.dart'; +import 'package:device_management/src/features/contacts/presentation/providers/contacts_controller.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockContactsRepository extends Mock implements ContactsRepository {} + +void main() { + setUpAll(() { + registerFallbackValue( + const CreateContactRequestDto( + id: 'x', + name: 'n', + phone: 'p', + userId: 'u', + ), + ); + registerFallbackValue( + const UpdateContactRequestDto(id: 'x', name: 'n', phone: 'p'), + ); + }); + + ProviderContainer buildContainer(ContactsRepository repo) { + return makeContainer( + overrides: [ + contactsRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('ContactsController.createContact', () { + test('blocks when at max', () async { + final repo = MockContactsRepository(); + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).createContact( + userId: 'u1', + name: 'N', + phoneE164: '+34600000001', + currentCount: 10, + maxCount: 10, + ); + + final state = container.read(contactsControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + verifyNever(() => repo.createContact(request: any(named: 'request'))); + }); + + test('creates contact via repository on success', () async { + final repo = MockContactsRepository(); + when(() => repo.createContact(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).createContact( + userId: 'u1', + name: 'N', + phoneE164: '+34600000001', + currentCount: 5, + maxCount: 10, + ); + + expect( + container.read(contactsControllerProvider), + isA>(), + ); + verify(() => repo.createContact(request: any(named: 'request'))) + .called(1); + }); + }); + + group('ContactsController.deleteContact', () { + test('blocks when only one contact', () async { + final repo = MockContactsRepository(); + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).deleteContact( + userId: 'u1', + contactId: 'c1', + currentCount: 1, + ); + + final state = container.read(contactsControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + verifyNever(() => repo.deleteContact(contactId: any(named: 'contactId'))); + }); + + test('deletes via repository on success', () async { + final repo = MockContactsRepository(); + when(() => repo.deleteContact(contactId: any(named: 'contactId'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).deleteContact( + userId: 'u1', + contactId: 'c1', + currentCount: 3, + ); + + expect( + container.read(contactsControllerProvider), + isA>(), + ); + verify(() => repo.deleteContact(contactId: 'c1')).called(1); + }); + }); + + group('ContactsController.updateContact', () { + test('updates via repository on success', () async { + final repo = MockContactsRepository(); + when(() => repo.updateContact(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).updateContact( + userId: 'u1', + contactId: 'c1', + name: 'N', + phoneE164: '+34600000001', + ); + + expect( + container.read(contactsControllerProvider), + isA>(), + ); + expect( + container.read(contactsControllerProvider.notifier).lastAction, + ContactsAction.update, + ); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockContactsRepository(); + when(() => repo.updateContact(request: any(named: 'request'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container.read(contactsControllerProvider.notifier).updateContact( + userId: 'u1', + contactId: 'c1', + name: 'N', + phoneE164: '+34600000001', + ); + + expect( + container.read(contactsControllerProvider), + isA>(), + ); + }); + }); +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 070dfe0b..48bac5ed 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -586,6 +586,9 @@ "numberAdded": "Nummer erfolgreich hinzugefügt", "numberRemoved": "Nummer erfolgreich entfernt", "numberUpdated": "Nummer erfolgreich aktualisiert", + "contactAdded": "Kontakt erfolgreich hinzugefügt", + "contactUpdated": "Kontakt erfolgreich aktualisiert", + "contactDeleted": "Kontakt erfolgreich gelöscht", "editAllowedNumber": "Erlaubte Nummer bearbeiten", "addSosContact": "SOS-Kontakt hinzufügen", "noSosContacts": "Keine SOS-Kontakte konfiguriert", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 0fa43b87..95648f3e 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -762,6 +762,9 @@ "numberAdded": "Number added successfully", "numberRemoved": "Number removed successfully", "numberUpdated": "Number updated successfully", + "contactAdded": "Contact added successfully", + "contactUpdated": "Contact updated successfully", + "contactDeleted": "Contact deleted successfully", "editAllowedNumber": "Edit allowed number", "addSosContact": "Add SOS contact", "noSosContacts": "No SOS contacts configured", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index 6b27e809..e573b52a 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -763,6 +763,9 @@ "numberAdded": "Número añadido correctamente", "numberRemoved": "Número eliminado correctamente", "numberUpdated": "Número actualizado correctamente", + "contactAdded": "Contacto añadido correctamente", + "contactUpdated": "Contacto actualizado correctamente", + "contactDeleted": "Contacto eliminado correctamente", "editAllowedNumber": "Editar número permitido", "addSosContact": "Añadir contacto SOS", "noSosContacts": "No hay contactos SOS configurados", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index df4f94d0..7e68a462 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -586,6 +586,9 @@ "numberAdded": "Numéro ajouté avec succès", "numberRemoved": "Numéro supprimé avec succès", "numberUpdated": "Numéro mis à jour avec succès", + "contactAdded": "Contact ajouté avec succès", + "contactUpdated": "Contact mis à jour avec succès", + "contactDeleted": "Contact supprimé avec succès", "editAllowedNumber": "Modifier le numéro autorisé", "addSosContact": "Ajouter un contact SOS", "noSosContacts": "Aucun contact SOS configuré", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index dfa1c39b..c2025558 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -586,6 +586,9 @@ "numberAdded": "Numero aggiunto con successo", "numberRemoved": "Numero rimosso con successo", "numberUpdated": "Numero aggiornato con successo", + "contactAdded": "Contatto aggiunto con successo", + "contactUpdated": "Contatto aggiornato con successo", + "contactDeleted": "Contatto eliminato con successo", "editAllowedNumber": "Modifica numero consentito", "addSosContact": "Aggiungi contatto SOS", "noSosContacts": "Nessun contatto SOS configurato", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 3c67137e..da1989da 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -586,6 +586,9 @@ "numberAdded": "Número adicionado com sucesso", "numberRemoved": "Número removido com sucesso", "numberUpdated": "Número atualizado com sucesso", + "contactAdded": "Contacto adicionado com sucesso", + "contactUpdated": "Contacto atualizado com sucesso", + "contactDeleted": "Contacto eliminado com sucesso", "editAllowedNumber": "Editar número permitido", "addSosContact": "Adicionar contacto SOS", "noSosContacts": "Nenhum contacto SOS configurado", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index a57c2ed6..8d7f201d 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -200,8 +200,11 @@ class I18n { static const String close = 'close'; static const String confirm = 'confirm'; static const String connect = 'connect'; + static const String contactAdded = 'contactAdded'; + static const String contactDeleted = 'contactDeleted'; static const String contactName = 'contactName'; static const String contactTitle = 'contactTitle'; + static const String contactUpdated = 'contactUpdated'; static const String contactsAgendaTitle = 'contactsAgendaTitle'; static const String contactsEmpty = 'contactsEmpty'; static const String contactsEmptyHint = 'contactsEmptyHint';