import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/people/presentation/providers/people_ui_state.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; part 'people_view_detail.dart'; part 'people_view_dialogs.dart'; part 'people_view_list.dart'; enum _PersonQuickAction { capture, note, idea, reminder } class PeopleView extends ConsumerWidget { const PeopleView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); final String searchQuery = ref.watch(peopleSearchQueryProvider); final String? relationshipFilter = ref.watch( peopleRelationshipFilterProvider, ); final PeopleSortOption sortOption = ref.watch(peopleSortOptionProvider); return localData.when( data: (LocalDataState data) { final List allPeople = data.people; if (allPeople.isEmpty) { return _EmptyPeopleView(onAdd: () => _handleAddPerson(context, ref)); } final List people = _applyPeopleFilters( allPeople, searchQuery: searchQuery, relationshipFilter: relationshipFilter, sortOption: sortOption, ); final List relationshipOptions = _relationshipOptions( allPeople, ); if (people.isEmpty) { return _FilteredPeopleEmptyView( onClear: () { ref.read(peopleSearchQueryProvider.notifier).set(''); ref.read(peopleRelationshipFilterProvider.notifier).set(null); }, ); } final String selectedId = ref.watch(selectedPersonIdProvider) ?? people.first.id; final PersonProfile selected = people.firstWhere( (PersonProfile person) => person.id == selectedId, orElse: () => people.first, ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool isCompact = constraints.maxWidth < 860; if (isCompact) { return _buildCompactLayout( context, ref, data, people, selected, relationshipOptions, ); } return _buildWideLayout( context, ref, data, people, selected, relationshipOptions, ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return const Center(child: Text('Unable to load people')); }, ); } List _applyPeopleFilters( List people, { required String searchQuery, required String? relationshipFilter, required PeopleSortOption sortOption, }) { final String query = searchQuery.trim().toLowerCase(); final String? relationship = relationshipFilter?.trim().toLowerCase(); final List filtered = people .where((PersonProfile person) { if (relationship != null && relationship.isNotEmpty && person.relationship.trim().toLowerCase() != relationship) { return false; } if (query.isEmpty) { return true; } final String haystack = [ person.name, ...person.aliases, person.relationship, person.location ?? '', person.notes, ...person.tags, ].join(' ').toLowerCase(); return haystack.contains(query); }) .toList(growable: true); filtered.sort((PersonProfile a, PersonProfile b) { switch (sortOption) { case PeopleSortOption.affinity: final int score = b.affinityScore.compareTo(a.affinityScore); if (score != 0) { return score; } return a.name.toLowerCase().compareTo(b.name.toLowerCase()); case PeopleSortOption.nextMoment: final int next = a.nextMoment.compareTo(b.nextMoment); if (next != 0) { return next; } return a.name.toLowerCase().compareTo(b.name.toLowerCase()); case PeopleSortOption.alphabetical: return a.name.toLowerCase().compareTo(b.name.toLowerCase()); } }); return filtered; } List _relationshipOptions(List people) { final Set seen = {}; final List values = []; for (final PersonProfile person in people) { final String relationship = person.relationship.trim(); if (relationship.isEmpty) { continue; } final String key = relationship.toLowerCase(); if (seen.add(key)) { values.add(relationship); } } values.sort( (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase()), ); return values; } Future _handleAddPerson(BuildContext context, WidgetRef ref) async { final List existingPeople = ref.read(localRepositoryProvider).asData?.value.people ?? const []; final _PersonDraft? draft = await _showPersonEditor( context, title: 'Add Person', existingPeople: existingPeople, ); if (draft == null) { return; } await ref .read(localRepositoryProvider.notifier) .addPerson( name: draft.name, relationship: draft.relationship, notes: draft.notes, tags: draft.tags, location: draft.location, aliases: draft.aliases, ); final LocalDataState? latest = ref .read(localRepositoryProvider) .asData ?.value; if (latest == null) { return; } for (final PersonProfile person in latest.people) { final bool locationMatches = (person.location ?? '').trim() == (draft.location ?? '').trim(); if (person.name == draft.name && person.relationship == draft.relationship && locationMatches) { ref.read(selectedPersonIdProvider.notifier).select(person.id); break; } } } Future _handleEditPerson( BuildContext context, WidgetRef ref, PersonProfile person, ) async { final List existingPeople = ref.read(localRepositoryProvider).asData?.value.people ?? const []; final _PersonDraft? draft = await _showPersonEditor( context, title: 'Edit Person', initial: _PersonDraft( name: person.name, relationship: person.relationship, notes: person.notes, tags: person.tags, aliases: person.aliases, location: person.location, ), existingPeople: existingPeople, editingPersonId: person.id, ); if (draft == null) { return; } await ref .read(localRepositoryProvider.notifier) .updatePerson( person.copyWith( name: draft.name, relationship: draft.relationship, notes: draft.notes, tags: draft.tags, aliases: draft.aliases, location: draft.location, ), ); } Future _handleDeletePerson( BuildContext context, WidgetRef ref, String personId, ) async { final bool? confirmed = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Delete person?'), content: const Text( 'This will remove the person and related moments from local storage.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete'), ), ], ); }, ); if (confirmed != true) { return; } await ref.read(localRepositoryProvider.notifier).deletePerson(personId); ref.read(selectedPersonIdProvider.notifier).clear(); } Widget _buildWideLayout( BuildContext context, WidgetRef ref, LocalDataState data, List people, PersonProfile selected, List relationshipOptions, ) { return Padding( padding: const EdgeInsets.fromLTRB(28, 24, 28, 28), child: Row( children: [ Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _PeopleHeader( onAdd: () => _handleAddPerson(context, ref), onMerge: () => _handleMergePeople(context, ref, people), compact: false, ), const SizedBox(height: 12), _PeopleListControls( compact: false, relationshipOptions: relationshipOptions, ), const SizedBox(height: 20), Expanded( child: ListView.separated( itemCount: people.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (BuildContext context, int index) { final PersonProfile person = people[index]; return _PersonListItem( person: person, selectedId: selected.id, compact: false, onSelect: () { ref .read(selectedPersonIdProvider.notifier) .select(person.id); }, ); }, ), ), ], ), ), const SizedBox(width: 18), Expanded( flex: 6, child: FrostedCard( child: SingleChildScrollView( child: _PersonDetail( person: selected, data: data, onEdit: () => _handleEditPerson(context, ref, selected), onDelete: () => _handleDeletePerson(context, ref, selected.id), onQuickAction: (_PersonQuickAction action) => _handleQuickAction(context, ref, selected, action), ), ), ), ), ], ), ); } Widget _buildCompactLayout( BuildContext context, WidgetRef ref, LocalDataState data, List people, PersonProfile selected, List relationshipOptions, ) { return Padding( padding: const EdgeInsets.fromLTRB(18, 16, 18, 20), child: ListView( children: [ _PeopleHeader( onAdd: () => _handleAddPerson(context, ref), onMerge: () => _handleMergePeople(context, ref, people), compact: true, ), const SizedBox(height: 10), _PeopleListControls( compact: true, relationshipOptions: relationshipOptions, ), const SizedBox(height: 14), FrostedCard( child: _PersonDetail( person: selected, data: data, compact: true, onEdit: () => _handleEditPerson(context, ref, selected), onDelete: () => _handleDeletePerson(context, ref, selected.id), onQuickAction: (_PersonQuickAction action) => _handleQuickAction(context, ref, selected, action), ), ), const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Row( children: [ Text( 'All profiles', style: Theme.of(context).textTheme.titleMedium, ), const Spacer(), Text( '${people.length}', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), const SizedBox(height: 10), ...people.map( (PersonProfile person) => Padding( padding: const EdgeInsets.only(bottom: 10), child: _PersonListItem( person: person, selectedId: selected.id, compact: true, onSelect: () { ref.read(selectedPersonIdProvider.notifier).select(person.id); }, ), ), ), ], ), ); } Future _handleQuickAction( BuildContext context, WidgetRef ref, PersonProfile person, _PersonQuickAction action, ) async { switch (action) { case _PersonQuickAction.capture: final String? summary = await _showQuickTextCaptureDialog( context, title: 'Add Capture', hintText: 'What happened in this interaction?', ); if (summary == null) { return; } await ref .read(localRepositoryProvider.notifier) .addMoment(personId: person.id, summary: summary, type: 'capture'); return; case _PersonQuickAction.note: final String? note = await _showQuickTextCaptureDialog( context, title: 'Add Note', hintText: 'Write a quick context note...', ); if (note == null) { return; } await ref .read(localRepositoryProvider.notifier) .addMoment(personId: person.id, summary: note, type: 'note'); return; case _PersonQuickAction.idea: final _QuickIdeaDraft? idea = await showDialog<_QuickIdeaDraft>( context: context, builder: (BuildContext context) => const _QuickIdeaDialog(), ); if (idea == null) { return; } await ref .read(localRepositoryProvider.notifier) .addIdea( type: idea.type, title: idea.title, details: idea.details, personId: person.id, ); return; case _PersonQuickAction.reminder: final _QuickReminderDraft? reminder = await showDialog<_QuickReminderDraft>( context: context, builder: (BuildContext context) => const _QuickReminderDialog(), ); if (reminder == null) { return; } await ref .read(localRepositoryProvider.notifier) .addReminder( title: reminder.title, cadence: reminder.cadence, nextAt: reminder.nextAt, personId: person.id, ); return; } } Future _handleMergePeople( BuildContext context, WidgetRef ref, List people, ) async { if (people.length < 2) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Need at least two profiles to merge.')), ); return; } final List duplicateCandidates = _duplicateCandidates( people, ); if (duplicateCandidates.length < 2) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'No duplicate-name profiles found. Edit names first if needed.', ), ), ); return; } final _MergePeopleDraft? draft = await showDialog<_MergePeopleDraft>( context: context, builder: (BuildContext context) => _MergePeopleDialog(people: duplicateCandidates), ); if (draft == null) { return; } await ref .read(localRepositoryProvider.notifier) .mergePersonProfiles( sourcePersonId: draft.sourcePersonId, targetPersonId: draft.targetPersonId, ); ref.read(selectedPersonIdProvider.notifier).select(draft.targetPersonId); if (!context.mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Profiles merged successfully.')), ); } List _duplicateCandidates(List people) { final Map> grouped = >{}; for (final PersonProfile person in people) { final String key = _normalizeNameKey(person.name); if (key.isEmpty) { continue; } grouped.putIfAbsent(key, () => []).add(person); } final List result = []; for (final List group in grouped.values) { if (group.length > 1) { result.addAll(group); } } return result; } String _normalizeNameKey(String value) { return value .trim() .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]+'), ' ') .replaceAll(RegExp(r'\s+'), ' '); } Future<_PersonDraft?> _showPersonEditor( BuildContext context, { required String title, _PersonDraft? initial, List existingPeople = const [], String? editingPersonId, }) { final bool compact = MediaQuery.sizeOf(context).width < 720; if (compact) { return showModalBottomSheet<_PersonDraft>( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { return _PersonEditorBottomSheet( title: title, initial: initial, existingPeople: existingPeople, editingPersonId: editingPersonId, ); }, ); } return showDialog<_PersonDraft>( context: context, builder: (BuildContext context) { return _PersonEditorDialog( title: title, initial: initial, existingPeople: existingPeople, editingPersonId: editingPersonId, ); }, ); } Future _showQuickTextCaptureDialog( BuildContext context, { required String title, required String hintText, }) async { return showDialog( context: context, builder: (BuildContext context) { return _QuickTextCaptureDialog(title: title, hintText: hintText); }, ); } }