// ignore_for_file: sort_constructors_first import 'package:flutter/foundation.dart'; enum IdeaType { gift, event } enum ReminderCadence { daily, weekly, monthly } @immutable class PersonProfile { const PersonProfile({ required this.id, required this.name, required this.relationship, required this.affinityScore, required this.nextMoment, required this.tags, required this.notes, this.location, }); final String id; final String name; final String relationship; final int affinityScore; final DateTime nextMoment; final List tags; final String notes; final String? location; PersonProfile copyWith({ String? id, String? name, String? relationship, int? affinityScore, DateTime? nextMoment, List? tags, String? notes, String? location, }) { return PersonProfile( id: id ?? this.id, name: name ?? this.name, relationship: relationship ?? this.relationship, affinityScore: affinityScore ?? this.affinityScore, nextMoment: nextMoment ?? this.nextMoment, tags: tags ?? this.tags, notes: notes ?? this.notes, location: location ?? this.location, ); } Map toJson() { return { 'id': id, 'name': name, 'relationship': relationship, 'affinityScore': affinityScore, 'nextMoment': nextMoment.toUtc().toIso8601String(), 'tags': tags, 'notes': notes, 'location': location, }; } factory PersonProfile.fromJson(Map json) { return PersonProfile( id: json['id'] as String, name: json['name'] as String, relationship: json['relationship'] as String, affinityScore: (json['affinityScore'] as num).toInt(), nextMoment: DateTime.parse(json['nextMoment'] as String).toLocal(), tags: (json['tags'] as List? ?? []) .map((dynamic tag) => '$tag') .toList(growable: false), notes: json['notes'] as String? ?? '', location: json['location'] as String?, ); } } @immutable class RelationshipMoment { const RelationshipMoment({ required this.id, required this.personId, required this.title, required this.summary, required this.at, required this.type, }); final String id; final String personId; final String title; final String summary; final DateTime at; final String type; RelationshipMoment copyWith({ String? id, String? personId, String? title, String? summary, DateTime? at, String? type, }) { return RelationshipMoment( id: id ?? this.id, personId: personId ?? this.personId, title: title ?? this.title, summary: summary ?? this.summary, at: at ?? this.at, type: type ?? this.type, ); } Map toJson() { return { 'id': id, 'personId': personId, 'title': title, 'summary': summary, 'at': at.toUtc().toIso8601String(), 'type': type, }; } factory RelationshipMoment.fromJson(Map json) { return RelationshipMoment( id: json['id'] as String, personId: json['personId'] as String, title: json['title'] as String, summary: json['summary'] as String, at: DateTime.parse(json['at'] as String).toLocal(), type: json['type'] as String, ); } } @immutable class RelationshipIdea { const RelationshipIdea({ required this.id, required this.type, required this.title, required this.details, required this.createdAt, this.personId, this.isArchived = false, }); final String id; final String? personId; final IdeaType type; final String title; final String details; final DateTime createdAt; final bool isArchived; RelationshipIdea copyWith({ String? id, String? personId, IdeaType? type, String? title, String? details, DateTime? createdAt, bool? isArchived, }) { return RelationshipIdea( id: id ?? this.id, personId: personId ?? this.personId, type: type ?? this.type, title: title ?? this.title, details: details ?? this.details, createdAt: createdAt ?? this.createdAt, isArchived: isArchived ?? this.isArchived, ); } Map toJson() { return { 'id': id, 'personId': personId, 'type': type.name, 'title': title, 'details': details, 'createdAt': createdAt.toUtc().toIso8601String(), 'isArchived': isArchived, }; } factory RelationshipIdea.fromJson(Map json) { final String typeName = json['type'] as String? ?? IdeaType.gift.name; return RelationshipIdea( id: json['id'] as String, personId: json['personId'] as String?, type: IdeaType.values.firstWhere( (IdeaType type) => type.name == typeName, orElse: () => IdeaType.gift, ), title: json['title'] as String, details: json['details'] as String? ?? '', createdAt: DateTime.parse(json['createdAt'] as String).toLocal(), isArchived: json['isArchived'] as bool? ?? false, ); } } @immutable class ReminderRule { const ReminderRule({ required this.id, required this.title, required this.cadence, required this.nextAt, this.personId, this.enabled = true, }); final String id; final String? personId; final String title; final ReminderCadence cadence; final DateTime nextAt; final bool enabled; ReminderRule copyWith({ String? id, String? personId, String? title, ReminderCadence? cadence, DateTime? nextAt, bool? enabled, }) { return ReminderRule( id: id ?? this.id, personId: personId ?? this.personId, title: title ?? this.title, cadence: cadence ?? this.cadence, nextAt: nextAt ?? this.nextAt, enabled: enabled ?? this.enabled, ); } Map toJson() { return { 'id': id, 'personId': personId, 'title': title, 'cadence': cadence.name, 'nextAt': nextAt.toUtc().toIso8601String(), 'enabled': enabled, }; } factory ReminderRule.fromJson(Map json) { final String cadenceName = json['cadence'] as String? ?? ReminderCadence.weekly.name; return ReminderRule( id: json['id'] as String, personId: json['personId'] as String?, title: json['title'] as String, cadence: ReminderCadence.values.firstWhere( (ReminderCadence cadence) => cadence.name == cadenceName, orElse: () => ReminderCadence.weekly, ), nextAt: DateTime.parse(json['nextAt'] as String).toLocal(), enabled: json['enabled'] as bool? ?? true, ); } } @immutable class DashboardTask { const DashboardTask({ required this.id, required this.title, required this.description, required this.when, this.done = false, }); final String id; final String title; final String description; final DateTime when; final bool done; DashboardTask copyWith({ String? id, String? title, String? description, DateTime? when, bool? done, }) { return DashboardTask( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, when: when ?? this.when, done: done ?? this.done, ); } Map toJson() { return { 'id': id, 'title': title, 'description': description, 'when': when.toUtc().toIso8601String(), 'done': done, }; } factory DashboardTask.fromJson(Map json) { return DashboardTask( id: json['id'] as String, title: json['title'] as String, description: json['description'] as String, when: DateTime.parse(json['when'] as String).toLocal(), done: json['done'] as bool? ?? false, ); } } @immutable class SourceProfileLink { const SourceProfileLink({ required this.id, required this.sourceApp, required this.normalizedDisplayName, required this.profileId, required this.firstSeenAt, required this.lastSeenAt, this.sourceUserId, this.sourceThreadId, }); final String id; final String sourceApp; final String? sourceUserId; final String? sourceThreadId; final String normalizedDisplayName; final String profileId; final DateTime firstSeenAt; final DateTime lastSeenAt; SourceProfileLink copyWith({ String? id, String? sourceApp, String? sourceUserId, String? sourceThreadId, String? normalizedDisplayName, String? profileId, DateTime? firstSeenAt, DateTime? lastSeenAt, }) { return SourceProfileLink( id: id ?? this.id, sourceApp: sourceApp ?? this.sourceApp, sourceUserId: sourceUserId ?? this.sourceUserId, sourceThreadId: sourceThreadId ?? this.sourceThreadId, normalizedDisplayName: normalizedDisplayName ?? this.normalizedDisplayName, profileId: profileId ?? this.profileId, firstSeenAt: firstSeenAt ?? this.firstSeenAt, lastSeenAt: lastSeenAt ?? this.lastSeenAt, ); } Map toJson() { return { 'id': id, 'sourceApp': sourceApp, 'sourceUserId': sourceUserId, 'sourceThreadId': sourceThreadId, 'normalizedDisplayName': normalizedDisplayName, 'profileId': profileId, 'firstSeenAt': firstSeenAt.toUtc().toIso8601String(), 'lastSeenAt': lastSeenAt.toUtc().toIso8601String(), }; } factory SourceProfileLink.fromJson(Map json) { return SourceProfileLink( id: json['id'] as String, sourceApp: json['sourceApp'] as String, sourceUserId: json['sourceUserId'] as String?, sourceThreadId: json['sourceThreadId'] as String?, normalizedDisplayName: json['normalizedDisplayName'] as String? ?? '', profileId: json['profileId'] as String, firstSeenAt: DateTime.parse(json['firstSeenAt'] as String).toLocal(), lastSeenAt: DateTime.parse(json['lastSeenAt'] as String).toLocal(), ); } } @immutable class SharedMessageEntry { const SharedMessageEntry({ required this.id, required this.sourceApp, required this.profileId, required this.messageText, required this.sharedAt, required this.importedAt, required this.resolvedAutomatically, this.sourceDisplayName, this.sourceUserId, this.sourceThreadId, }); final String id; final String sourceApp; final String profileId; final String messageText; final DateTime sharedAt; final DateTime importedAt; final bool resolvedAutomatically; final String? sourceDisplayName; final String? sourceUserId; final String? sourceThreadId; SharedMessageEntry copyWith({ String? id, String? sourceApp, String? profileId, String? messageText, DateTime? sharedAt, DateTime? importedAt, bool? resolvedAutomatically, String? sourceDisplayName, String? sourceUserId, String? sourceThreadId, }) { return SharedMessageEntry( id: id ?? this.id, sourceApp: sourceApp ?? this.sourceApp, profileId: profileId ?? this.profileId, messageText: messageText ?? this.messageText, sharedAt: sharedAt ?? this.sharedAt, importedAt: importedAt ?? this.importedAt, resolvedAutomatically: resolvedAutomatically ?? this.resolvedAutomatically, sourceDisplayName: sourceDisplayName ?? this.sourceDisplayName, sourceUserId: sourceUserId ?? this.sourceUserId, sourceThreadId: sourceThreadId ?? this.sourceThreadId, ); } Map toJson() { return { 'id': id, 'sourceApp': sourceApp, 'profileId': profileId, 'messageText': messageText, 'sharedAt': sharedAt.toUtc().toIso8601String(), 'importedAt': importedAt.toUtc().toIso8601String(), 'resolvedAutomatically': resolvedAutomatically, 'sourceDisplayName': sourceDisplayName, 'sourceUserId': sourceUserId, 'sourceThreadId': sourceThreadId, }; } factory SharedMessageEntry.fromJson(Map json) { return SharedMessageEntry( id: json['id'] as String, sourceApp: json['sourceApp'] as String, profileId: json['profileId'] as String, messageText: json['messageText'] as String? ?? '', sharedAt: DateTime.parse(json['sharedAt'] as String).toLocal(), importedAt: DateTime.parse(json['importedAt'] as String).toLocal(), resolvedAutomatically: json['resolvedAutomatically'] as bool? ?? true, sourceDisplayName: json['sourceDisplayName'] as String?, sourceUserId: json['sourceUserId'] as String?, sourceThreadId: json['sourceThreadId'] as String?, ); } } @immutable class DashboardSummary { const DashboardSummary({ required this.activePeople, required this.upcomingPlans, required this.pendingIdeas, required this.weeklyConsistency, }); final int activePeople; final int upcomingPlans; final int pendingIdeas; final int weeklyConsistency; } @immutable class LocalDataState { const LocalDataState({ required this.people, required this.moments, required this.ideas, required this.reminders, required this.tasks, this.sourceLinks = const [], this.sharedMessages = const [], }); final List people; final List moments; final List ideas; final List reminders; final List tasks; final List sourceLinks; final List sharedMessages; LocalDataState copyWith({ List? people, List? moments, List? ideas, List? reminders, List? tasks, List? sourceLinks, List? sharedMessages, }) { return LocalDataState( people: people ?? this.people, moments: moments ?? this.moments, ideas: ideas ?? this.ideas, reminders: reminders ?? this.reminders, tasks: tasks ?? this.tasks, sourceLinks: sourceLinks ?? this.sourceLinks, sharedMessages: sharedMessages ?? this.sharedMessages, ); } DashboardSummary summary({DateTime? now}) { final DateTime baseline = now ?? DateTime.now(); return DashboardSummary( activePeople: people.length, upcomingPlans: people .where((PersonProfile p) => p.nextMoment.isAfter(baseline)) .length, pendingIdeas: ideas .where((RelationshipIdea idea) => !idea.isArchived) .length, weeklyConsistency: moments.isEmpty ? 0 : (70 + moments.length * 4).clamp(0, 100), ); } Map toJson() { return { 'people': people .map((PersonProfile person) => person.toJson()) .toList(growable: false), 'moments': moments .map((RelationshipMoment moment) => moment.toJson()) .toList(growable: false), 'ideas': ideas .map((RelationshipIdea idea) => idea.toJson()) .toList(growable: false), 'reminders': reminders .map((ReminderRule reminder) => reminder.toJson()) .toList(growable: false), 'tasks': tasks .map((DashboardTask task) => task.toJson()) .toList(growable: false), 'sourceLinks': sourceLinks .map((SourceProfileLink link) => link.toJson()) .toList(growable: false), 'sharedMessages': sharedMessages .map((SharedMessageEntry entry) => entry.toJson()) .toList(growable: false), }; } factory LocalDataState.fromJson(Map json) { return LocalDataState( people: (json['people'] as List? ?? []) .map( (dynamic person) => PersonProfile.fromJson(person as Map), ) .toList(growable: false), moments: (json['moments'] as List? ?? []) .map( (dynamic moment) => RelationshipMoment.fromJson(moment as Map), ) .toList(growable: false), ideas: (json['ideas'] as List? ?? []) .map( (dynamic idea) => RelationshipIdea.fromJson(idea as Map), ) .toList(growable: false), reminders: (json['reminders'] as List? ?? []) .map( (dynamic reminder) => ReminderRule.fromJson(reminder as Map), ) .toList(growable: false), tasks: (json['tasks'] as List? ?? []) .map( (dynamic task) => DashboardTask.fromJson(task as Map), ) .toList(growable: false), sourceLinks: (json['sourceLinks'] as List? ?? []) .map( (dynamic link) => SourceProfileLink.fromJson(link as Map), ) .toList(growable: false), sharedMessages: (json['sharedMessages'] as List? ?? []) .map( (dynamic entry) => SharedMessageEntry.fromJson(entry as Map), ) .toList(growable: false), ); } static LocalDataState seed() { return LocalDataState( people: [ PersonProfile( id: 'p-ava', name: 'Ava Hart', relationship: 'Partner', affinityScore: 92, nextMoment: DateTime(2026, 2, 16, 19, 30), tags: ['coffee', 'books', 'slow mornings'], notes: 'Prefers thoughtful plans over expensive plans.', location: 'Austin, TX', ), PersonProfile( id: 'p-jordan', name: 'Jordan Lee', relationship: 'Close Friend', affinityScore: 78, nextMoment: DateTime(2026, 2, 18, 18, 0), tags: ['running', 'street food'], notes: 'Has a race on Sunday; ask how training is going.', location: 'Chicago, IL', ), PersonProfile( id: 'p-mila', name: 'Mila Stone', relationship: 'Sister', affinityScore: 84, nextMoment: DateTime(2026, 2, 20, 12, 0), tags: ['plants', 'retro music'], notes: 'Birthday prep should start this week.', location: 'Seattle, WA', ), ], moments: [ RelationshipMoment( id: 'm-1', personId: 'p-ava', title: 'Sunday Walk Tradition', summary: 'Short walk + no-phone hour worked really well.', at: DateTime(2026, 2, 9, 9, 30), type: 'ritual', ), RelationshipMoment( id: 'm-2', personId: 'p-jordan', title: 'Post-workout Check-in', summary: 'Shared training playlist and grabbed smoothies.', at: DateTime(2026, 2, 11, 19, 0), type: 'support', ), RelationshipMoment( id: 'm-3', personId: 'p-mila', title: 'Gift Idea Captured', summary: 'Found a vintage lamp from her saved style board.', at: DateTime(2026, 2, 12, 14, 20), type: 'gift', ), ], ideas: [ RelationshipIdea( id: 'i-1', personId: 'p-ava', type: IdeaType.gift, title: 'Handwritten recipe journal', details: 'Collect 10 memories and recipes from this year.', createdAt: DateTime(2026, 2, 12, 9), ), RelationshipIdea( id: 'i-2', personId: 'p-mila', type: IdeaType.event, title: 'Plant market + brunch date', details: 'Saturday morning slot; book nearby cafe in advance.', createdAt: DateTime(2026, 2, 13, 11), ), ], reminders: [ ReminderRule( id: 'r-1', personId: 'p-jordan', title: 'Weekly check-in message', cadence: ReminderCadence.weekly, nextAt: DateTime(2026, 2, 18, 18), ), ReminderRule( id: 'r-2', personId: 'p-ava', title: 'Sunday ritual planning', cadence: ReminderCadence.weekly, nextAt: DateTime(2026, 2, 16, 10), ), ], tasks: [ DashboardTask( id: 't-1', title: 'Plan Friday dinner with Ava', description: 'Keep it low-key: ramen + bookstore stop.', when: DateTime(2026, 2, 16, 17, 0), ), DashboardTask( id: 't-2', title: 'Send race-day encouragement to Jordan', description: 'Voice note before 8:00 AM.', when: DateTime(2026, 2, 17, 7, 30), ), DashboardTask( id: 't-3', title: 'Finalize Mila birthday shortlist', description: 'Pick 1 meaningful gift and 1 backup.', when: DateTime(2026, 2, 18, 20, 0), ), ], ); } }