part of 'people_view.dart'; class _EmptyPeopleView extends StatelessWidget { const _EmptyPeopleView({required this.onAdd}); final VoidCallback onAdd; @override Widget build(BuildContext context) { return Center( child: FrostedCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'No people yet', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Add the first relationship profile to start tracking moments.', style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 12), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add Person'), ), ], ), ), ); } } class _FilteredPeopleEmptyView extends StatelessWidget { const _FilteredPeopleEmptyView({required this.onClear}); final VoidCallback onClear; @override Widget build(BuildContext context) { return Center( child: FrostedCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.search_off_rounded, size: 34, color: AppTheme.textSecondary, ), const SizedBox(height: 10), Text( 'No matching people', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 6), Text( 'Try another search, relationship filter, or clear the current filters.', style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: onClear, icon: const Icon(Icons.restart_alt_rounded), label: const Text('Clear Filters'), ), ], ), ), ); } } class _PeopleListControls extends ConsumerWidget { const _PeopleListControls({ required this.compact, required this.relationshipOptions, }); final bool compact; final List relationshipOptions; @override Widget build(BuildContext context, WidgetRef ref) { final String query = ref.watch(peopleSearchQueryProvider); final String? selectedRelationship = ref.watch( peopleRelationshipFilterProvider, ); final PeopleSortOption sortOption = ref.watch(peopleSortOptionProvider); return FrostedCard( padding: EdgeInsets.all(compact ? 12 : 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: TextFormField( key: ValueKey('people-search-$query'), initialValue: query, onChanged: (String value) { ref.read(peopleSearchQueryProvider.notifier).set(value); }, decoration: const InputDecoration( hintText: 'Search name, tags, notes, location', prefixIcon: Icon(Icons.search_rounded), isDense: true, border: OutlineInputBorder(), ), ), ), const SizedBox(width: 8), PopupMenuButton( tooltip: 'Sort', onSelected: (PeopleSortOption value) { ref.read(peopleSortOptionProvider.notifier).set(value); }, itemBuilder: (BuildContext context) => >[ const PopupMenuItem( value: PeopleSortOption.affinity, child: Text('Sort by affinity'), ), const PopupMenuItem( value: PeopleSortOption.nextMoment, child: Text('Sort by next moment'), ), const PopupMenuItem( value: PeopleSortOption.alphabetical, child: Text('Sort A-Z'), ), ], child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), decoration: BoxDecoration( color: const Color(0xFFF3F8FB), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.sort_rounded, size: 18), if (!compact) ...[ const SizedBox(width: 6), Text(switch (sortOption) { PeopleSortOption.affinity => 'Affinity', PeopleSortOption.nextMoment => 'Next', PeopleSortOption.alphabetical => 'A-Z', }), ], ], ), ), ), ], ), const SizedBox(height: 10), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _FilterChipButton( label: 'All', selected: selectedRelationship == null, onTap: () { ref .read(peopleRelationshipFilterProvider.notifier) .set(null); }, ), ...relationshipOptions.map( (String relationship) => Padding( padding: const EdgeInsets.only(left: 8), child: _FilterChipButton( label: relationship, selected: selectedRelationship?.toLowerCase() == relationship.toLowerCase(), onTap: () { final PeopleRelationshipFilterNotifier controller = ref .read(peopleRelationshipFilterProvider.notifier); final bool isSelected = selectedRelationship?.toLowerCase() == relationship.toLowerCase(); controller.set(isSelected ? null : relationship); }, ), ), ), ], ), ), ], ), ); } } class _FilterChipButton extends StatelessWidget { const _FilterChipButton({ required this.label, required this.selected, required this.onTap, }); final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(99), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: selected ? const Color(0xFFEAF7FB) : const Color(0xFFF4F8FB), borderRadius: BorderRadius.circular(99), border: Border.all( color: selected ? const Color(0xFFB8E4EE) : Colors.white.withValues(alpha: 0.8), ), ), child: Text( label, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: selected ? AppTheme.primary : AppTheme.textSecondary, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, ), ), ), ); } } class _PeopleHeader extends StatelessWidget { const _PeopleHeader({ required this.onAdd, required this.onMerge, required this.compact, }); final VoidCallback onAdd; final VoidCallback onMerge; final bool compact; @override Widget build(BuildContext context) { final TextTheme textTheme = Theme.of(context).textTheme; if (compact) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('People', style: textTheme.headlineMedium), const SizedBox(height: 6), Text( 'Track what matters to each relationship and follow through.', style: textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add person'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: onMerge, icon: const Icon(Icons.merge_type_rounded), label: const Text('Merge duplicates'), ), ], ); } return Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('People', style: textTheme.headlineMedium), const SizedBox(height: 6), Text( 'Track what matters to each relationship and follow through.', style: textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), FilledButton.icon( onPressed: onAdd, icon: const Icon(Icons.person_add_alt_1_rounded), label: const Text('Add'), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: onMerge, icon: const Icon(Icons.merge_type_rounded), label: const Text('Merge'), ), ], ); } } class _PersonListItem extends StatelessWidget { const _PersonListItem({ required this.person, required this.selectedId, required this.compact, required this.onSelect, }); final PersonProfile person; final String selectedId; final bool compact; final VoidCallback onSelect; @override Widget build(BuildContext context) { final bool isSelected = person.id == selectedId; return InkWell( onTap: onSelect, borderRadius: BorderRadius.circular(20), child: FrostedCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _AvatarSeed(name: person.name), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( person.relationship, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), _ScorePill(score: person.affinityScore), if (isSelected) const Padding( padding: EdgeInsets.only(left: 10), child: Icon(Icons.check_circle, color: Color(0xFF1AB6C8)), ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ _InlineMetaPill( icon: Icons.schedule_rounded, label: _dateTimeLabel(person.nextMoment), ), if ((person.location ?? '').trim().isNotEmpty) _InlineMetaPill( icon: Icons.location_on_outlined, label: person.location!.trim(), ), ...person.tags .take(compact ? 2 : 3) .map( (String tag) => _InlineMetaPill( icon: Icons.sell_outlined, label: tag, ), ), ], ), ], ), ), ); } } class _InlineMetaPill extends StatelessWidget { const _InlineMetaPill({required this.icon, required this.label}); final IconData icon; final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( color: const Color(0xFFF1F7FA), borderRadius: BorderRadius.circular(99), border: Border.all(color: Colors.white), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: AppTheme.textSecondary), const SizedBox(width: 6), Flexible( child: Text( label, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ), ], ), ); } } class _AvatarSeed extends StatelessWidget { const _AvatarSeed({required this.name, this.size = 48}); final String name; final double size; @override Widget build(BuildContext context) { final String initials = name .split(' ') .where((String part) => part.isNotEmpty) .take(2) .map((String part) => part[0].toUpperCase()) .join(); return Container( width: size, height: size, decoration: BoxDecoration( borderRadius: BorderRadius.circular(size / 2), gradient: const LinearGradient( colors: [Color(0xFF1AB6C8), Color(0xFF2274E0)], ), ), alignment: Alignment.center, child: Text( initials, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ); } } class _ScorePill extends StatelessWidget { const _ScorePill({required this.score}); final int score; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFEEF7F3), borderRadius: BorderRadius.circular(99), ), child: Text( '$score%', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: const Color(0xFF1D9C66), fontWeight: FontWeight.w700, ), ), ); } } String _dateTimeLabel(DateTime value) { final DateTime local = value.toLocal(); final String month = local.month.toString().padLeft(2, '0'); final String day = local.day.toString().padLeft(2, '0'); final String hour = local.hour.toString().padLeft(2, '0'); final String minute = local.minute.toString().padLeft(2, '0'); return '$month/$day $hour:$minute'; } String _shortDateTimeLabel(DateTime value) { final DateTime local = value.toLocal(); final String year = local.year.toString(); final String month = local.month.toString().padLeft(2, '0'); final String day = local.day.toString().padLeft(2, '0'); final String hour = local.hour.toString().padLeft(2, '0'); final String minute = local.minute.toString().padLeft(2, '0'); return '$year-$month-$day $hour:$minute'; }