// MOCK DATA — TEMPORARY. See MockSwitch.dart. import 'dart:math'; import '../about/external/initial/GroupGoalRequest.dart'; import '../about/external/initial/GroupMessageRequest.dart'; import '../about/external/initial/GroupRequest.dart'; import '../about/external/initial/JoinGroupRequest.dart'; import '../about/external/initial/NudgeRequest.dart'; import '../about/external/initial/ObjectiveProgressRequest.dart'; import '../utils/Colors.dart'; import '../utils/CommonUtils.dart'; import 'MockStore.dart'; /// The fake backend's groups: a couple already joined, one waiting behind an /// invite code, and members who answer in the chat a moment after you post. class MockGroups { static const String _me = "mock-user"; static const String _myName = "Alex"; static bool _seeded = false; static final List> _groups = >[]; /// Groups that exist but you are not in yet, keyed by invite code. static final Map> _joinable = >{}; static final Map>> _messages = >>{}; /// Replies that "arrive" once their time has passed. static final List> _pending = >[]; static final Random _random = Random(7); static Map _member(String id, String name, {String role = "Member", double kept = 0.7, int streak = 2, String standing = "Good", int checkedInHoursAgo = 5}) { return { 'id': id, 'name': name, 'role': role, 'keptRate': kept, 'streak': streak, 'standing': standing, 'lastCheckIn': DateTime.now() .subtract(Duration(hours: checkedInHoursAgo)) .toIso8601String(), 'me': id == _me, }; } static Map _objective( String id, String title, String unit, double target, double current, double mine) { return { 'id': id, 'title': title, 'unit': unit, 'target': target, 'current': current, 'mine': mine, }; } static Map _message(String groupId, String authorId, String authorName, String body, int minutesAgo, {String kind = "Text"}) { return { 'id': MockStore.newId('msg'), 'groupId': groupId, 'authorId': authorId, 'authorName': authorName, 'body': body, 'kind': kind, 'sentAt': DateTime.now() .subtract(Duration(minutes: minutesAgo)) .toIso8601String(), 'mine': authorId == _me, }; } static void _ensureSeeded() { if (_seeded) { return; } _seeded = true; final DateTime now = DateTime.now(); _groups.addAll(>[ { 'id': 'group-run', 'name': 'Morning Crew', 'description': 'Out the door before 7, five days a week.', 'colourHex': colorToHex(goalAccents[1]), 'inviteCode': 'MRN7AM', 'unread': 3, 'members': >[ _member('m-sam', 'Sam', role: 'Owner', kept: 0.92, streak: 11, checkedInHoursAgo: 2), _member(_me, _myName, kept: 0.78, streak: 4, checkedInHoursAgo: 4), _member('m-priya', 'Priya', kept: 0.85, streak: 7, checkedInHoursAgo: 3), _member('m-jordan', 'Jordan', kept: 0.41, streak: 0, standing: 'Warned', checkedInHoursAgo: 52), ], 'goals': >[ { 'id': 'gg-run', 'title': 'Run 200 km this month', 'description': 'Every kilometre counts, treadmill included.', 'targetDate': DateTime(now.year, now.month + 1, 0).toIso8601String(), 'colourHex': colorToHex(goalAccents[1]), 'objectives': >[ _objective('ob-km', 'Distance run', 'km', 200, 86, 22), _objective('ob-sessions', 'Runs logged', 'runs', 40, 17, 5), ], }, { 'id': 'gg-sleep', 'title': 'In bed before 23:00', 'description': 'Mornings start the night before.', 'targetDate': '', 'colourHex': colorToHex(goalAccents[4]), 'objectives': >[ _objective('ob-nights', 'Early nights', 'nights', 60, 31, 9), ], }, ], }, { 'id': 'group-thesis', 'name': 'Thesis Sprint', 'description': 'Drafts in by the 15th. No heroics, just pages.', 'colourHex': colorToHex(goalAccents[3]), 'inviteCode': 'THS15D', 'unread': 0, 'members': >[ _member(_me, _myName, role: 'Owner', kept: 0.78, streak: 4, checkedInHoursAgo: 4), _member('m-mei', 'Mei', kept: 0.88, streak: 9, checkedInHoursAgo: 6), _member('m-tom', 'Tom', kept: 0.63, streak: 1, checkedInHoursAgo: 20), ], 'goals': >[ { 'id': 'gg-drafts', 'title': 'Full drafts submitted', 'description': 'Three theses, one deadline.', 'targetDate': DateTime(now.year, now.month + 1, 15).toIso8601String(), 'colourHex': colorToHex(goalAccents[3]), 'objectives': >[ _objective('ob-words', 'Words written', 'words', 30000, 18400, 7200), _objective('ob-chapters', 'Chapters reviewed', 'chapters', 9, 3, 1), _objective('ob-hours', 'Deep-work hours', 'hours', 120, 64, 23), ], }, ], }, ]); _joinable['WLK2GO'] = { 'id': 'group-walk', 'name': 'Weekend Walkers', 'description': 'Ten thousand steps, Saturday and Sunday, no excuses.', 'colourHex': colorToHex(goalAccents[5]), 'inviteCode': 'WLK2GO', 'unread': 1, 'members': >[ _member('m-ade', 'Ade', role: 'Owner', kept: 0.81, streak: 6), _member('m-lena', 'Lena', kept: 0.74, streak: 3), ], 'goals': >[ { 'id': 'gg-steps', 'title': '1 million steps together', 'description': '', 'targetDate': '', 'colourHex': colorToHex(goalAccents[5]), 'objectives': >[ _objective('ob-steps', 'Steps', 'steps', 1000000, 412000, 0), ], }, ], }; _messages['group-run'] = >[ _message('group-run', 'm-sam', 'Sam', 'Same route tomorrow? Gate at 6:15.', 60 * 20), _message('group-run', _me, _myName, "I'm in. Bringing Priya's spare torch.", 60 * 19 + 40), _message('group-run', 'm-priya', 'Priya', 'logged 6 km on Distance run', 60 * 3, kind: 'CheckIn'), _message('group-run', 'm-sam', 'Sam', 'logged 8 km on Distance run', 60 * 2, kind: 'CheckIn'), _message('group-run', 'm-sam', 'Sam', 'nudged Jordan: two days without a run, we miss you out there', 95, kind: 'Nudge'), _message('group-run', 'm-priya', 'Priya', '86 km down. We need about 9 a day to make it, doable.', 40), ]; _messages['group-thesis'] = >[ _message('group-thesis', _me, _myName, 'started the group', 60 * 24 * 6, kind: 'System'), _message('group-thesis', 'm-mei', 'Mei', 'Chapter 3 is with my supervisor. Terrified.', 60 * 26), _message('group-thesis', _me, _myName, 'logged 1,200 words on Words written', 60 * 22, kind: 'CheckIn'), _message('group-thesis', 'm-tom', 'Tom', 'Library 9 to 1 tomorrow if anyone wants a co-working block.', 60 * 7), ]; _messages['group-walk'] = >[ _message('group-walk', 'm-ade', 'Ade', 'Welcome! Saturday loop starts at the bridge, 9:00.', 60 * 30), ]; } static Map? _find(String id) { for (Map group in _groups) { if (group['id'] == id) { return group; } } return null; } /// Moves any replies whose moment has come into their threads. static void _deliver() { final DateTime now = DateTime.now(); final List> due = _pending .where((item) => DateTime.parse(item['sentAt']).isBefore(now)) .toList(); for (Map message in due) { _pending.remove(message); _messages.putIfAbsent(message['groupId'], () => >[]) .add(message); final Map? group = _find(message['groupId']); if (group != null) { group['unread'] = (group['unread'] ?? 0) + 1; } } } static Map _withLast(Map group) { final List> thread = _messages[group['id']] ?? >[]; final Map copy = Map.from(group); if (thread.isNotEmpty) { final Map last = thread.last; copy['lastMessage'] = last['body']; copy['lastAuthor'] = last['authorName']; copy['lastMessageAt'] = last['sentAt']; } return copy; } static void _post(String groupId, String body, String kind) { _messages.putIfAbsent(groupId, () => >[]).add( _message(groupId, _me, _myName, body, 0, kind: kind)); } /// Someone answers a few seconds later, so the chat feels inhabited. static void _queueReply(String groupId) { final Map? group = _find(groupId); if (group == null) { return; } final List others = (group['members'] as List) .where((member) => member['id'] != _me) .toList(); if (others.isEmpty) { return; } const List lines = [ "Love it. Keep that going.", "Same here, just finished mine.", "That's the spirit.", "Noted. I'll match you tomorrow.", "Good — that puts us back on pace.", "Who's in for the next one?", ]; final Map author = others[_random.nextInt(others.length)]; final Map reply = _message(groupId, author['id'], author['name'], lines[_random.nextInt(lines.length)], 0); reply['sentAt'] = DateTime.now().add(const Duration(seconds: 3)).toIso8601String(); _pending.add(reply); } static String _code() { const String letters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; return List.generate( 6, (index) => letters[_random.nextInt(letters.length)]).join(); } // ── Endpoints ───────────────────────────────────────────────────────────── static List> mine() { _ensureSeeded(); _deliver(); return _groups.map(_withLast).toList(); } static Map one(String id) { _ensureSeeded(); _deliver(); return _withLast(_find(id) ?? {}); } static Map save(GroupRequest request) { _ensureSeeded(); final Map? existing = request.id == null ? null : _find(request.id!); if (existing != null) { existing['name'] = request.name; existing['description'] = request.description; existing['colourHex'] = request.colourHex; return _withLast(existing); } final String id = MockStore.newId('group'); final Map group = { 'id': id, 'name': request.name, 'description': request.description, 'colourHex': request.colourHex, 'inviteCode': _code(), 'unread': 0, 'members': >[ _member(_me, _myName, role: 'Owner', kept: 0.78, streak: 4, checkedInHoursAgo: 0), ], 'goals': >[], }; _groups.insert(0, group); _post(id, 'started the group', 'System'); return _withLast(group); } static Map join(JoinGroupRequest request) { _ensureSeeded(); final String code = request.inviteCode.trim().toUpperCase(); for (Map group in _groups) { if (group['inviteCode'] == code) { return _withLast(group); } } // Any well-formed code finds the waiting group, so the preview always // has something to join. final Map group = _joinable.remove(code) ?? (_joinable.isNotEmpty ? _joinable.remove(_joinable.keys.first)! : { 'id': MockStore.newId('group'), 'name': 'Group $code', 'description': '', 'colourHex': colorToHex(goalAccents[0]), 'inviteCode': code, 'unread': 0, 'members': >[], 'goals': >[], }); (group['members'] as List).add(_member(_me, _myName, kept: 0.78, streak: 4, checkedInHoursAgo: 0)); _groups.insert(0, group); _post(group['id'], 'joined the group', 'System'); _queueReply(group['id']); return _withLast(group); } static Map leave(String id) { _ensureSeeded(); _groups.removeWhere((group) => group['id'] == id); return {'success': true}; } static Map saveGoal(GroupGoalRequest request) { _ensureSeeded(); final Map? group = _find(request.groupId); if (group == null) { return {}; } final Map goal = { 'id': MockStore.newId('gg'), 'title': request.title, 'description': request.description, 'targetDate': request.targetDate, 'colourHex': request.colourHex, 'objectives': request.objectives .map((draft) => _objective(MockStore.newId('ob'), draft.title, draft.unit, draft.target, 0, 0)) .toList(), }; (group['goals'] as List).add(goal); _post(group['id'], 'added a shared goal: ${request.title}', 'System'); return _withLast(group); } static Map logProgress(ObjectiveProgressRequest request) { _ensureSeeded(); final Map? group = _find(request.groupId); if (group == null) { return {}; } for (Map goal in (group['goals'] as List)) { if (goal['id'] != request.goalId) { continue; } for (Map objective in (goal['objectives'] as List)) { if (objective['id'] != request.objectiveId) { continue; } objective['current'] = (objective['current'] as num) + request.amount; objective['mine'] = (objective['mine'] as num) + request.amount; final String amount = formatAmount(request.amount); _post( group['id'], 'logged $amount ${objective['unit']} on ${objective['title']}' '${request.note.isEmpty ? "" : " — ${request.note}"}', 'CheckIn'); _queueReply(group['id']); } } for (Map member in (group['members'] as List)) { if (member['id'] == _me) { member['lastCheckIn'] = DateTime.now().toIso8601String(); } } return _withLast(group); } static List> messages(String groupId) { _ensureSeeded(); _deliver(); final Map? group = _find(groupId); if (group != null) { group['unread'] = 0; } return List>.from( _messages[groupId] ?? >[]); } static Map send(GroupMessageRequest request) { _ensureSeeded(); _post(request.groupId, request.body, request.kind); _queueReply(request.groupId); return _messages[request.groupId]!.last; } static Map nudge(NudgeRequest request) { _ensureSeeded(); final Map? group = _find(request.groupId); if (group == null) { return {'success': false}; } String who = "everyone"; for (Map member in (group['members'] as List)) { if (member['id'] == request.memberId) { who = member['name']; } } _post(request.groupId, 'nudged $who: today still counts, go get it', 'Nudge'); return {'success': true}; } }