import 'dart:math' as math; 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/shared/frosted_card.dart'; import 'package:relationship_saver/features/signals/signals_controller.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class DashboardView extends ConsumerWidget { const DashboardView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); return localData.when( data: (LocalDataState data) { final DashboardSummary summary = data.summary(); final List tasks = data.tasks; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 720; return SingleChildScrollView( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 16 : 24, compact ? 16 : 28, compact ? 24 : 36, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Today', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( 'See the relationship graph first, then execute the highest-leverage next moves.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 18), _RelationshipGraphCard(people: data.people, compact: compact), const SizedBox(height: 20), Wrap( spacing: 12, runSpacing: 12, children: [ _StatCard( label: 'Active People', value: '${summary.activePeople}', accent: const Color(0xFF16B8CA), compact: compact, ), _StatCard( label: 'Upcoming Plans', value: '${summary.upcomingPlans}', accent: const Color(0xFF2B7FFF), compact: compact, ), _StatCard( label: 'Pending Ideas', value: '${summary.pendingIdeas}', accent: const Color(0xFFFFA447), compact: compact, ), _StatCard( label: 'Weekly Rhythm', value: '${summary.weeklyConsistency}%', accent: const Color(0xFF30B66A), compact: compact, ), ], ), const SizedBox(height: 24), FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Next Moves', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 14), if (tasks.isEmpty) Text( 'No tasks yet. Add moments, ideas, or reminders to generate follow-through actions.', style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: AppTheme.textSecondary), ) else ...tasks.map( (DashboardTask task) => _TaskRow( task: task, compact: compact, onToggleDone: () { ref .read(localRepositoryProvider.notifier) .toggleTaskDone(task.id); }, ), ), ], ), ), ], ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return Center( child: Text( 'Could not load local data.', style: Theme.of(context).textTheme.bodyLarge, ), ); }, ); } } class _RelationshipGraphCard extends StatelessWidget { const _RelationshipGraphCard({required this.people, required this.compact}); final List people; final bool compact; @override Widget build(BuildContext context) { return FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( 'Relationship Graph', style: Theme.of(context).textTheme.titleLarge, ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFEAF7FB), borderRadius: BorderRadius.circular(99), ), child: Text( '${people.length} nodes', style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.primary), ), ), ], ), const SizedBox(height: 12), Container( height: compact ? 330 : 410, decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFFEAF3F8), const Color(0xFFF7FAFC), Colors.white.withValues(alpha: 0.9), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.withValues(alpha: 0.8)), ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { if (people.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'No people yet. Add your first relationship to start the graph.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge ?.copyWith(color: AppTheme.textSecondary), ), ), ); } final _GraphLayout layout = _GraphLayout.build( people: people, size: constraints.biggest, compact: compact, ); return InteractiveViewer( minScale: 0.65, maxScale: 2.8, boundaryMargin: const EdgeInsets.all(220), panEnabled: true, scaleEnabled: true, child: SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: Stack( children: [ Positioned.fill( child: CustomPaint( painter: _RelationshipEdgePainter( center: layout.center, edges: layout.edges, ), ), ), ...layout.nodes.map( (_GraphNode node) => _GraphBubble( node: node, compact: compact, onLongPress: () => _openPersonInsights(context, node.person), ), ), _CenterNode(center: layout.center, compact: compact), if (layout.hiddenCount > 0) Positioned( right: 12, top: 12, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(99), ), child: Text( '+${layout.hiddenCount} more', style: Theme.of(context).textTheme.labelLarge ?.copyWith(color: AppTheme.textSecondary), ), ), ), ], ), ), ); }, ), ), ), const SizedBox(height: 8), Text( 'Pinch to zoom, drag to pan, long-press a person to open insights.', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: _collectLegend(people) .map( (_RelationshipVisual visual) => _RelationshipLegendPill(visual: visual), ) .toList(growable: false), ), ], ), ); } List<_RelationshipVisual> _collectLegend(List source) { final Set seen = {}; final List<_RelationshipVisual> result = <_RelationshipVisual>[]; for (final PersonProfile person in source) { final _RelationshipVisual visual = _relationshipVisualFor( person.relationship, ); if (seen.add(visual.label)) { result.add(visual); } if (result.length >= 7) { break; } } if (result.isEmpty) { result.add(_otherVisual); } return result; } void _openPersonInsights(BuildContext context, PersonProfile person) { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { return _PersonInsightsPage(personId: person.id); }, ), ); } } class _GraphLayout { const _GraphLayout({ required this.center, required this.nodes, required this.edges, required this.hiddenCount, }); final Offset center; final List<_GraphNode> nodes; final List<_GraphEdge> edges; final int hiddenCount; static _GraphLayout build({ required List people, required Size size, required bool compact, }) { final int maxNodes = compact ? 10 : 16; final List visiblePeople = people .take(maxNodes) .toList(growable: false); final int hiddenCount = people.length - visiblePeople.length; final Offset center = Offset(size.width / 2, size.height / 2); if (visiblePeople.isEmpty) { return _GraphLayout( center: center, nodes: const <_GraphNode>[], edges: const <_GraphEdge>[], hiddenCount: hiddenCount, ); } final double minSide = math.min(size.width, size.height); final int total = visiblePeople.length; final int innerCount = total <= 8 ? total : 8; final int outerCount = total - innerCount; final double innerRadius = minSide * (compact ? 0.28 : 0.31); final double outerRadius = minSide * (compact ? 0.39 : 0.43); final List<_GraphNode> nodes = <_GraphNode>[]; for (int i = 0; i < total; i += 1) { final bool inOuterRing = i >= innerCount; final int ringCount = inOuterRing ? outerCount : innerCount; final int ringIndex = inOuterRing ? i - innerCount : i; final double radius = inOuterRing ? outerRadius : innerRadius; final double phaseOffset = inOuterRing ? math.pi / ringCount : 0; final double angle = (-math.pi / 2) + phaseOffset + ((2 * math.pi * ringIndex) / ringCount); final Offset position = Offset( center.dx + radius * math.cos(angle), center.dy + radius * math.sin(angle), ); final PersonProfile person = visiblePeople[i]; final _RelationshipVisual visual = _relationshipVisualFor( person.relationship, ); nodes.add(_GraphNode(person: person, visual: visual, position: position)); } final List<_GraphEdge> edges = <_GraphEdge>[]; for (final _GraphNode node in nodes) { edges.add( _GraphEdge( start: center, end: node.position, color: node.visual.color.withValues(alpha: 0.46), width: compact ? 1.7 : 2.0, ), ); } if (nodes.length > 2) { for (int i = 0; i < nodes.length; i += 1) { final _GraphNode current = nodes[i]; final _GraphNode next = nodes[(i + 1) % nodes.length]; final Color color = Color.lerp(current.visual.color, next.visual.color, 0.5) ?? AppTheme.accent; edges.add( _GraphEdge( start: current.position, end: next.position, color: color.withValues(alpha: 0.2), width: 1.1, ), ); if (nodes.length >= 7 && i.isEven) { final _GraphNode jump = nodes[(i + 3) % nodes.length]; edges.add( _GraphEdge( start: current.position, end: jump.position, color: current.visual.color.withValues(alpha: 0.1), width: 1.0, ), ); } } } return _GraphLayout( center: center, nodes: nodes, edges: edges, hiddenCount: hiddenCount, ); } } class _GraphNode { const _GraphNode({ required this.person, required this.visual, required this.position, }); final PersonProfile person; final _RelationshipVisual visual; final Offset position; } class _GraphEdge { const _GraphEdge({ required this.start, required this.end, required this.color, required this.width, }); final Offset start; final Offset end; final Color color; final double width; } class _RelationshipEdgePainter extends CustomPainter { const _RelationshipEdgePainter({required this.center, required this.edges}); final Offset center; final List<_GraphEdge> edges; @override void paint(Canvas canvas, Size size) { final Paint centerGlow = Paint() ..shader = RadialGradient( colors: [ AppTheme.accent.withValues(alpha: 0.15), Colors.transparent, ], ).createShader( Rect.fromCircle( center: center, radius: math.min(size.width, size.height) * 0.36, ), ); canvas.drawCircle( center, math.min(size.width, size.height) * 0.36, centerGlow, ); for (final _GraphEdge edge in edges) { final Paint paint = Paint() ..color = edge.color ..strokeWidth = edge.width ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; canvas.drawLine(edge.start, edge.end, paint); } } @override bool shouldRepaint(covariant _RelationshipEdgePainter oldDelegate) { return oldDelegate.edges != edges || oldDelegate.center != center; } } class _GraphBubble extends StatelessWidget { const _GraphBubble({ required this.node, required this.compact, required this.onLongPress, }); final _GraphNode node; final bool compact; final VoidCallback onLongPress; @override Widget build(BuildContext context) { final double diameter = compact ? 52 : 62; final double labelWidth = compact ? 66 : 86; return Positioned( left: node.position.dx - (diameter / 2), top: node.position.dy - (diameter / 2), child: Tooltip( message: '${node.person.name} • ${node.visual.label}', child: GestureDetector( key: ValueKey('graph-node-${node.person.id}'), behavior: HitTestBehavior.translucent, onLongPress: onLongPress, child: SizedBox( width: labelWidth, child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: diameter, height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [ _shiftLightness(node.visual.color, 0.16), _shiftLightness(node.visual.color, -0.12), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: node.visual.color.withValues(alpha: 0.24), blurRadius: 14, offset: const Offset(0, 8), ), ], ), alignment: Alignment.center, child: Text( _initials(node.person.name), style: Theme.of(context).textTheme.titleMedium ?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), Positioned( right: -2, top: -2, child: Container( width: compact ? 20 : 22, height: compact ? 20 : 22, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: node.visual.color.withValues(alpha: 0.8), ), ), child: Icon( node.visual.icon, size: compact ? 12 : 13, color: node.visual.color, ), ), ), ], ), if (!compact) ...[ const SizedBox(height: 5), Text( _shortName(node.person.name), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: AppTheme.textSecondary, ), ), ], ], ), ), ), ), ); } } class _CenterNode extends StatelessWidget { const _CenterNode({required this.center, required this.compact}); final Offset center; final bool compact; @override Widget build(BuildContext context) { final double diameter = compact ? 74 : 86; return Positioned( left: center.dx - (diameter / 2), top: center.dy - (diameter / 2), child: Container( width: diameter, height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: const LinearGradient( colors: [Color(0xFF1AB6C8), Color(0xFF286DE0)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: const Color(0xFF1AB6C8).withValues(alpha: 0.26), blurRadius: 20, offset: const Offset(0, 10), ), ], ), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.favorite_rounded, color: Colors.white, size: 18), const SizedBox(height: 2), Text( 'YOU', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, letterSpacing: 0.4, ), ), ], ), ), ); } } class _RelationshipLegendPill extends StatelessWidget { const _RelationshipLegendPill({required this.visual}); final _RelationshipVisual visual; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: visual.color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(99), border: Border.all(color: visual.color.withValues(alpha: 0.18)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(visual.icon, size: 14, color: visual.color), const SizedBox(width: 6), Text( visual.label, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ], ), ); } } class _PersonInsightsPage extends ConsumerWidget { const _PersonInsightsPage({required this.personId}); final String personId; @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue localData = ref.watch( localRepositoryProvider, ); final AsyncValue signals = ref.watch( signalsControllerProvider, ); return Scaffold( appBar: AppBar(title: const Text('Person Insights')), body: localData.when( data: (LocalDataState data) { final PersonProfile? person = _findPersonById(data.people, personId); if (person == null) { return Center( child: Text( 'Person was not found in local data.', style: Theme.of(context).textTheme.bodyLarge, ), ); } final List moments = data.moments .where( (RelationshipMoment moment) => moment.personId == person.id, ) .toList(growable: false) ..sort( (RelationshipMoment a, RelationshipMoment b) => b.at.compareTo(a.at), ); final List ideas = data.ideas .where((RelationshipIdea idea) => idea.personId == person.id) .toList(growable: false) ..sort( (RelationshipIdea a, RelationshipIdea b) => b.createdAt.compareTo(a.createdAt), ); final List reminders = data.reminders .where( (ReminderRule reminder) => reminder.personId == person.id, ) .toList(growable: false) ..sort( (ReminderRule a, ReminderRule b) => a.nextAt.compareTo(b.nextAt), ); final List relatedTasks = _findTasksForPerson( data.tasks, person, ); final List personSignals = signals.asData?.value.items .where((SignalItem item) => item.personId == person.id) .toList(growable: false) ?? const []; return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 760; return SingleChildScrollView( padding: EdgeInsets.fromLTRB( compact ? 16 : 24, compact ? 16 : 20, compact ? 16 : 24, compact ? 24 : 30, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _personProfileCard(context, person, compact), const SizedBox(height: 14), _InsightSectionCard( title: 'Ideas', icon: Icons.lightbulb_outline_rounded, children: ideas.isEmpty ? [ const _SectionEmpty( label: 'No captured ideas yet for this relationship.', ), ] : ideas .map( (RelationshipIdea idea) => _InsightRow( title: idea.title, subtitle: idea.details, meta: '${idea.type.name.toUpperCase()} • ${_shortDate(idea.createdAt)}', ), ) .toList(growable: false), ), const SizedBox(height: 12), _InsightSectionCard( title: 'Promotions & Signals', icon: Icons.local_offer_outlined, children: _signalRows( context: context, signals: personSignals, sourceState: signals, ), ), const SizedBox(height: 12), _InsightSectionCard( title: 'Moments', icon: Icons.auto_awesome_rounded, children: moments.isEmpty ? [ const _SectionEmpty( label: 'No logged moments for this person yet.', ), ] : moments .map( (RelationshipMoment moment) => _InsightRow( title: moment.title, subtitle: moment.summary, meta: '${moment.type.toUpperCase()} • ${_shortDate(moment.at)}', ), ) .toList(growable: false), ), const SizedBox(height: 12), _InsightSectionCard( title: 'Reminders', icon: Icons.notifications_active_outlined, children: reminders.isEmpty ? [ const _SectionEmpty( label: 'No reminders configured for this relationship.', ), ] : reminders .map( (ReminderRule reminder) => _InsightRow( title: reminder.title, subtitle: reminder.enabled ? 'Enabled' : 'Disabled', meta: '${reminder.cadence.name.toUpperCase()} • Next ${_shortDate(reminder.nextAt)}', ), ) .toList(growable: false), ), const SizedBox(height: 12), _InsightSectionCard( title: 'Tasks & Follow-Ups', icon: Icons.checklist_rounded, children: relatedTasks.isEmpty ? [ const _SectionEmpty( label: 'No direct follow-up tasks found for this person.', ), ] : relatedTasks .map( (DashboardTask task) => _InsightRow( title: task.title, subtitle: task.description, meta: '${task.done ? 'DONE' : 'PENDING'} • ${_shortDate(task.when)}', ), ) .toList(growable: false), ), ], ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return Center( child: Text( 'Could not load person insights.', style: Theme.of(context).textTheme.bodyLarge, ), ); }, ), ); } Widget _personProfileCard( BuildContext context, PersonProfile person, bool compact, ) { final _RelationshipVisual visual = _relationshipVisualFor( person.relationship, ); return FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (compact) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _insightAvatar(person: person, visual: visual), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 3), Text( person.relationship, style: Theme.of(context).textTheme.bodyMedium ?.copyWith(color: AppTheme.textSecondary), ), ], ), ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ _miniMetric( label: 'Affinity', value: '${person.affinityScore}%', ), _miniMetric( label: 'Next Moment', value: _shortDate(person.nextMoment), ), ], ), ], ) else Row( children: [ _insightAvatar(person: person, visual: visual), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( person.name, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 3), Text( person.relationship, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), _miniMetric( label: 'Affinity', value: '${person.affinityScore}%', ), const SizedBox(width: 10), _miniMetric( label: 'Next Moment', value: _shortDate(person.nextMoment), ), ], ), if (person.tags.isNotEmpty) ...[ const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: person.tags .map((String tag) => _TagPill(label: tag)) .toList(growable: false), ), ], if (person.notes.trim().isNotEmpty) ...[ const SizedBox(height: 12), Text( person.notes, style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), ), ], ], ), ); } Widget _insightAvatar({ required PersonProfile person, required _RelationshipVisual visual, }) { return Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [ _shiftLightness(visual.color, 0.16), _shiftLightness(visual.color, -0.12), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), alignment: Alignment.center, child: Text( _initials(person.name), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16, ), ), ); } Widget _miniMetric({required String label, required String value}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFF2F8FB), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), const SizedBox(height: 2), Text( value, style: const TextStyle( fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), ], ), ); } List _signalRows({ required BuildContext context, required List signals, required AsyncValue sourceState, }) { if (sourceState.isLoading && sourceState.asData == null) { return const [ Padding( padding: EdgeInsets.symmetric(vertical: 8), child: LinearProgressIndicator(minHeight: 3), ), ]; } if (signals.isEmpty) { if (sourceState.hasError) { return [ const _SectionEmpty( label: 'Signals unavailable right now. Backend sync can repopulate.', ), ]; } return [ const _SectionEmpty( label: 'No promotions/signals targeted to this person yet.', ), ]; } return signals .map( (SignalItem item) => _InsightRow( title: item.title, subtitle: item.description ?? 'No description', meta: '${item.type.toUpperCase()} • ${_shortDate(item.createdAt)}', ), ) .toList(growable: false); } PersonProfile? _findPersonById(List people, String id) { for (final PersonProfile person in people) { if (person.id == id) { return person; } } return null; } List _findTasksForPerson( List tasks, PersonProfile person, ) { final List tokens = person.name .toLowerCase() .split(' ') .where((String token) => token.length > 2) .toList(growable: false); if (tokens.isEmpty) { return const []; } return tasks .where((DashboardTask task) { final String fullText = '${task.title} ${task.description}' .toLowerCase(); for (final String token in tokens) { if (fullText.contains(token)) { return true; } } return false; }) .toList(growable: false) ..sort((DashboardTask a, DashboardTask b) => a.when.compareTo(b.when)); } } class _InsightSectionCard extends StatelessWidget { const _InsightSectionCard({ required this.title, required this.icon, required this.children, }); final String title; final IconData icon; final List children; @override Widget build(BuildContext context) { return FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 6, crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon(icon, size: 18, color: AppTheme.primary), Text(title, style: Theme.of(context).textTheme.titleMedium), ], ), const SizedBox(height: 10), ...children, ], ), ); } } class _InsightRow extends StatelessWidget { const _InsightRow({ required this.title, required this.subtitle, required this.meta, }); final String title; final String subtitle; final String meta; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF5FAFB), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 4), Text( subtitle, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 6), Text( meta, style: Theme.of( context, ).textTheme.labelSmall?.copyWith(color: AppTheme.textSecondary), ), ], ), ), ); } } class _SectionEmpty extends StatelessWidget { const _SectionEmpty({required this.label}); final String label; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFFF5FAFB), borderRadius: BorderRadius.circular(12), ), child: Text( label, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), ); } } class _TagPill extends StatelessWidget { const _TagPill({required this.label}); final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFFEFF7FA), borderRadius: BorderRadius.circular(99), ), child: Text( label, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ); } } class _StatCard extends StatelessWidget { const _StatCard({ required this.label, required this.value, required this.accent, required this.compact, }); final String label; final String value; final Color accent; final bool compact; @override Widget build(BuildContext context) { return FrostedCard( padding: const EdgeInsets.all(16), child: SizedBox( width: compact ? double.infinity : 188, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 8, width: 40, decoration: BoxDecoration( color: accent, borderRadius: BorderRadius.circular(99), ), ), const SizedBox(height: 16), Text( value, style: Theme.of( context, ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 4), Text( label, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), ], ), ), ); } } class _TaskRow extends StatelessWidget { const _TaskRow({ required this.task, required this.onToggleDone, required this.compact, }); final DashboardTask task; final VoidCallback onToggleDone; final bool compact; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFF5FAFB), borderRadius: BorderRadius.circular(16), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: onToggleDone, borderRadius: BorderRadius.circular(20), child: Icon( task.done ? Icons.check_circle_rounded : Icons.radio_button_unchecked_rounded, color: task.done ? const Color(0xFF1D9C66) : const Color(0xFF1AB6C8), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( task.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( decoration: task.done ? TextDecoration.lineThrough : null, ), ), const SizedBox(height: 4), Text( task.description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ), ], ), ), if (!compact) ...[ const SizedBox(width: 12), Text( _shortDate(task.when), style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), ], ], ), ), ); } } class _RelationshipVisual { const _RelationshipVisual({ required this.label, required this.icon, required this.color, }); final String label; final IconData icon; final Color color; } const _RelationshipVisual _spouseVisual = _RelationshipVisual( label: 'Spouse', icon: Icons.favorite_rounded, color: Color(0xFFE0557B), ); const _RelationshipVisual _fianceVisual = _RelationshipVisual( label: 'Fiance', icon: Icons.diamond_rounded, color: Color(0xFFCB6CF2), ); const _RelationshipVisual _partnerVisual = _RelationshipVisual( label: 'Partner', icon: Icons.favorite_border_rounded, color: Color(0xFFE67A5A), ); const _RelationshipVisual _bestieVisual = _RelationshipVisual( label: 'Bestie', icon: Icons.emoji_emotions_rounded, color: Color(0xFF8E6BFF), ); const _RelationshipVisual _friendVisual = _RelationshipVisual( label: 'Friend', icon: Icons.groups_rounded, color: Color(0xFF3F8CFF), ); const _RelationshipVisual _familyVisual = _RelationshipVisual( label: 'Family', icon: Icons.family_restroom_rounded, color: Color(0xFF2FB67D), ); const _RelationshipVisual _sisterVisual = _RelationshipVisual( label: 'Sister', icon: Icons.female_rounded, color: Color(0xFFFF6EA9), ); const _RelationshipVisual _brotherVisual = _RelationshipVisual( label: 'Brother', icon: Icons.male_rounded, color: Color(0xFF4A7DFF), ); const _RelationshipVisual _otherVisual = _RelationshipVisual( label: 'Connection', icon: Icons.hub_rounded, color: Color(0xFF5F7280), ); _RelationshipVisual _relationshipVisualFor(String relationship) { final String value = relationship.toLowerCase(); if (_containsAny(value, ['spouse', 'wife', 'husband'])) { return _spouseVisual; } if (_containsAny(value, ['fiance', 'engaged'])) { return _fianceVisual; } if (_containsAny(value, [ 'girlfriend', 'boyfriend', 'partner', 'gf', 'bf', ])) { return _partnerVisual; } if (_containsAny(value, ['bestie', 'best friend', 'bff'])) { return _bestieVisual; } if (_containsAny(value, ['sister', 'sis'])) { return _sisterVisual; } if (_containsAny(value, ['brother', 'bro'])) { return _brotherVisual; } if (_containsAny(value, [ 'family', 'mother', 'father', 'mom', 'dad', 'parent', 'aunt', 'uncle', 'cousin', 'grand', ])) { return _familyVisual; } if (_containsAny(value, ['friend', 'pal', 'buddy', 'mate'])) { return _friendVisual; } return _otherVisual; } bool _containsAny(String value, List needles) { for (final String needle in needles) { if (value.contains(needle)) { return true; } } return false; } Color _shiftLightness(Color color, double delta) { final HSLColor hsl = HSLColor.fromColor(color); final double next = (hsl.lightness + delta).clamp(0.0, 1.0); return hsl.withLightness(next).toColor(); } String _initials(String name) { final List parts = name .split(' ') .where((String part) => part.isNotEmpty) .toList(growable: false); if (parts.isEmpty) { return '?'; } if (parts.length == 1) { return parts.first .substring(0, math.min(2, parts.first.length)) .toUpperCase(); } return '${parts.first[0]}${parts[1][0]}'.toUpperCase(); } String _shortName(String name) { final List parts = name .split(' ') .where((String part) => part.isNotEmpty) .toList(growable: false); if (parts.isEmpty) { return 'Unknown'; } return parts.first; } String _shortDate(DateTime value) { const List weekdays = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', ]; final String weekday = weekdays[value.weekday - 1]; final String day = value.day.toString().padLeft(2, '0'); return '$weekday $day'; }