From b31bc47cc0a5ebca59c21dad591bfb3851cbf76c Mon Sep 17 00:00:00 2001 From: alvocool Date: Mon, 2 Nov 2026 08:45:41 +0300 Subject: [PATCH] Add removable mock mode for a clickable preview Runs the whole app on seeded in-memory data with no backend or device storage, behind useMockData in lib/Grounded/mock/MockSwitch.dart. To remove: delete lib/Grounded/mock/ and the lines tagged "MOCK:" in ParentViewModel (or revert this commit, keeping the notification guard). Also skips local notifications on web, where the plugin and dart:io Platform both throw. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw --- .../configs/NotificationServiceConfig.dart | 18 +- frontend/lib/Grounded/mock/MockComms.dart | 443 ++++++++++++++++++ frontend/lib/Grounded/mock/MockMemory.dart | 26 + frontend/lib/Grounded/mock/MockStorage.dart | 55 +++ frontend/lib/Grounded/mock/MockStore.dart | 298 ++++++++++++ frontend/lib/Grounded/mock/MockSwitch.dart | 8 + .../Grounded/see/parent/ParentViewModel.dart | 14 +- 7 files changed, 860 insertions(+), 2 deletions(-) create mode 100644 frontend/lib/Grounded/mock/MockComms.dart create mode 100644 frontend/lib/Grounded/mock/MockMemory.dart create mode 100644 frontend/lib/Grounded/mock/MockStorage.dart create mode 100644 frontend/lib/Grounded/mock/MockStore.dart create mode 100644 frontend/lib/Grounded/mock/MockSwitch.dart diff --git a/frontend/lib/Grounded/configs/NotificationServiceConfig.dart b/frontend/lib/Grounded/configs/NotificationServiceConfig.dart index 6ee72c4..a3667a7 100644 --- a/frontend/lib/Grounded/configs/NotificationServiceConfig.dart +++ b/frontend/lib/Grounded/configs/NotificationServiceConfig.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../about/external/data/Commitment.dart'; @@ -32,7 +33,7 @@ class LocalNotificationEngine { static bool _ready = false; static Future init() async { - if (_ready) { + if (_ready || kIsWeb) { return; } @@ -105,6 +106,10 @@ class LocalNotificationEngine { /// The ongoing notification for a running task. Shows the live remaining /// time so it is useful from the lock screen without unlocking. static Future showSessionNotification(LiveSession session) async { + // The browser has no notification channels; the runner works without. + if (kIsWeb) { + return; + } await init(); final String remaining = session.requiredSeconds > 0 @@ -144,6 +149,10 @@ class LocalNotificationEngine { } static Future cancelSessionNotification() async { + // The browser has no notification channels; the runner works without. + if (kIsWeb) { + return; + } await init(); await plugin.cancel(id: sessionNotificationId); } @@ -156,6 +165,9 @@ class LocalNotificationEngine { EscalationTier tier, ToneLevel tone, ) async { + if (kIsWeb) { + return; + } await init(); final bool nonNegotiable = @@ -198,6 +210,10 @@ class LocalNotificationEngine { } static Future cancelAll() async { + // The browser has no notification channels; the runner works without. + if (kIsWeb) { + return; + } await init(); await plugin.cancelAll(); } diff --git a/frontend/lib/Grounded/mock/MockComms.dart b/frontend/lib/Grounded/mock/MockComms.dart new file mode 100644 index 0000000..a94319f --- /dev/null +++ b/frontend/lib/Grounded/mock/MockComms.dart @@ -0,0 +1,443 @@ +// MOCK DATA — TEMPORARY. See MockSwitch.dart. + +import 'package:dio/dio.dart'; + +import '../about/external/data/RoutineChain.dart'; +import '../about/external/data/pages/request/CommitmentsRequest.dart'; +import '../about/external/data/pages/request/HistoryRequest.dart'; +import '../about/external/initial/AbandonRequest.dart'; +import '../about/external/initial/AmnestyRequest.dart'; +import '../about/external/initial/CommitmentRequest.dart'; +import '../about/external/initial/CompletionRequest.dart'; +import '../about/external/initial/DeferralRequest.dart'; +import '../about/external/initial/DeviceRequest.dart'; +import '../about/external/initial/GoalRequest.dart'; +import '../about/external/initial/IdRequest.dart'; +import '../about/external/initial/LoginData.dart'; +import '../about/external/initial/ReportCardRequest.dart'; +import '../about/external/initial/SessionLogRequest.dart'; +import '../about/external/initial/SickModeRequest.dart'; +import '../about/external/initial/ToneRequest.dart'; +import '../comms/ConnectComms.dart'; +import 'MockStore.dart'; + +/// A fake backend. Answers every call from [MockStore] after a short delay, +/// so loading states still flash the way they will against the real thing. +class MockComms implements ConnectComms { + MockComms() { + MockStore.ensureSeeded(); + } + + Future _ok(dynamic data) async { + await Future.delayed(const Duration(milliseconds: 250)); + return Response( + requestOptions: RequestOptions(path: 'mock'), + statusCode: 200, + data: data, + ); + } + + Future _done() => _ok({'success': true}); + + Future _emptyPage() => _ok(MockStore.page(>[])); + + // ── Auth ────────────────────────────────────────────────────────────────── + + @override + Future registerDevice(DeviceRequest request) => _done(); + + @override + Future registerDeviceToken(String token) => _done(); + + @override + Future login(LoginData request) => _ok({ + 'id': 'mock-user', + 'name': 'Alex', + 'token': 'mock-token', + 'pic': '', + 'tone': 'Firm', + 'standing': 'Good', + 'amnestyTokens': 3, + 'sickMode': false, + }); + + @override + Future logout() => _done(); + + @override + Future me() => _ok({'id': 'mock-user', 'name': 'Alex'}); + + // ── Goals ───────────────────────────────────────────────────────────────── + + @override + Future getMyGoals(HistoryRequest request) => + _ok(MockStore.goalsWithCounts()); + + @override + Future saveGoalEntry(GoalRequest request) { + final Map json = request.toJson(); + json['targetDate'] = request.targetDate.isEmpty ? null : request.targetDate; + json['archived'] = false; + + final int index = + MockStore.goals.indexWhere((goal) => goal['id'] == request.id); + if (request.id == null || index < 0) { + json['id'] = MockStore.newId('goal'); + MockStore.goals.add(json); + } else { + MockStore.goals[index] = json; + } + return _ok(json); + } + + @override + Future getGoalTasks(IdRequest request) => + _ok(MockStore.goalTasks(request.id)); + + @override + Future archiveGoalEntry(IdRequest request) { + for (Map goal in MockStore.goals) { + if (goal['id'] == request.id) { + goal['archived'] = true; + } + } + return _done(); + } + + // ── Commitments ─────────────────────────────────────────────────────────── + + @override + Future getTodayPlan(CommitmentsRequest request) => + _ok(MockStore.page(MockStore.todayPlan())); + + @override + Future getMyCommitments(CommitmentsRequest request) => + _ok(MockStore.page(MockStore.commitments)); + + @override + Future getOverdueQueue(HistoryRequest request) => + _ok(MockStore.page(MockStore.overdue())); + + @override + Future saveCommitmentEntry(CommitmentRequest request) { + final Map json = request.toJson(); + json['id'] = MockStore.newId('task'); + json['status'] = 'Open'; + json['deferralCount'] = 0; + MockStore.commitments.add(json); + return _ok(json); + } + + @override + Future updateCommitmentEntry(CommitmentRequest request) { + final Map? item = MockStore.commitment(request.id ?? ''); + if (item != null) { + item.addAll(request.toJson()); + } + return _done(); + } + + @override + Future deleteCommitmentEntry(IdRequest request) { + MockStore.commitments.removeWhere((item) => item['id'] == request.id); + return _done(); + } + + @override + Future completeCommitmentEntry(CompletionRequest request) { + final Map? item = MockStore.commitment(request.commitmentId); + if (item != null) { + final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? ''); + item['status'] = end != null && end.isBefore(DateTime.now()) + ? 'LateCompleted' + : 'Completed'; + } + return _done(); + } + + @override + Future deferCommitmentEntry(DeferralRequest request) { + final Map? item = MockStore.commitment(request.commitmentId); + if (item != null) { + final DateTime now = DateTime.now(); + item['dueStart'] = request.newDueStart.isNotEmpty + ? request.newDueStart + : now.add(const Duration(hours: 1)).toIso8601String(); + item['dueEnd'] = request.newDueEnd.isNotEmpty + ? request.newDueEnd + : now.add(const Duration(days: 1)).toIso8601String(); + item['deferralCount'] = (item['deferralCount'] ?? 0) + 1; + item['status'] = 'Deferred'; + } + return _done(); + } + + @override + Future abandonCommitmentEntry(AbandonRequest request) { + final Map? item = MockStore.commitment(request.commitmentId); + if (item != null) { + item['status'] = 'Abandoned'; + } + return _done(); + } + + @override + Future getCommitmentHistory(HistoryRequest request) => _emptyPage(); + + @override + Future getCommitmentEvents(IdRequest request) => _emptyPage(); + + // ── Capacity ────────────────────────────────────────────────────────────── + + @override + Future checkCapacity(CommitmentsRequest request) => _done(); + + @override + Future getCapacityProfileEntry() => _ok({}); + + // ── Debt & standing ─────────────────────────────────────────────────────── + + @override + Future getDebtSummary() => _ok({}); + + @override + Future getDebtLedger(HistoryRequest request) => _emptyPage(); + + @override + Future getDebtTrend(ReportCardRequest request) => _ok([]); + + @override + Future getStanding() => _ok({'standing': 'Good'}); + + @override + Future getStandingHistory(HistoryRequest request) => _emptyPage(); + + // ── Excuses ─────────────────────────────────────────────────────────────── + + @override + Future getExcuseClusters(ReportCardRequest request) => + _ok(>[ + { + 'id': 'cluster-tired', + 'label': 'Too tired', + 'occurrences': 4, + 'dominantCategory': 'Deep work', + 'byWeekday': {'4': 3, '2': 1}, + 'byHour': {'19': 3, '21': 1}, + 'insight': + "You've said \"too tired\" 4 times this month — 3 of them on Thursday evenings. Put deep work before lunch on Thursdays.", + }, + { + 'id': 'cluster-time', + 'label': 'Ran out of time', + 'occurrences': 3, + 'dominantCategory': 'Admin', + 'byWeekday': {'1': 2, '5': 1}, + 'byHour': {'17': 3}, + 'insight': '', + }, + ]); + + // ── Proof ───────────────────────────────────────────────────────────────── + + @override + Future uploadPhotoProof(FormData request) => + _ok({'proofRef': 'mock-photo'}); + + @override + Future submitTimerProofEntry(CompletionRequest request) => + completeCommitmentEntry(request); + + @override + Future submitLocationProofEntry(CompletionRequest request) => + completeCommitmentEntry(request); + + // ── Guardrails ──────────────────────────────────────────────────────────── + + @override + Future spendAmnestyToken(AmnestyRequest request) => _done(); + + @override + Future getAmnestyBalance() => _ok({'balance': 3}); + + @override + Future updateSickMode(SickModeRequest request) => _done(); + + @override + Future checkDistress() => _ok({'distressed': false}); + + @override + Future updateTone(ToneRequest request) => _done(); + + // ── Habits & routines ───────────────────────────────────────────────────── + + @override + Future getMyHabits(HistoryRequest request) => _ok([]); + + @override + Future saveHabitEntry(Map request) => _done(); + + @override + Future logHabitEntry(IdRequest request) => _done(); + + @override + Future getKeystoneHabits() => _ok([]); + + @override + Future getMyRoutines(HistoryRequest request) => _ok( + MockStore.routines.map((routine) => routine.toJson()).toList()); + + @override + Future saveRoutineEntry(Map request) { + final RoutineChain routine = RoutineChain.fromJson(request); + final int index = MockStore.routines + .indexWhere((item) => item.id != null && item.id == routine.id); + if (routine.id == null || index < 0) { + routine.id = MockStore.newId('routine'); + MockStore.routines.add(routine); + } else { + // Keep today's progress across an edit. + final RoutineChain before = MockStore.routines[index]; + for (int step = 0; + step < routine.steps.length && step < before.steps.length; + step++) { + routine.steps[step].completed = before.steps[step].completed; + } + MockStore.routines[index] = routine; + } + return _ok(routine.toJson()); + } + + /// A logged run marks today's progress on the routine, so the library ring + /// and the "done" tick reflect what just happened. + @override + Future logRoutineChainEntry(Map request) { + for (RoutineChain routine in MockStore.routines) { + if (routine.id == request['routineId']) { + final int done = request['completedSteps'] ?? 0; + for (int step = 0; step < routine.steps.length; step++) { + routine.steps[step].completed = step < done; + } + routine.lastBreakIndex = request['breakIndex'] ?? -1; + } + } + return _done(); + } + + // ── Training ────────────────────────────────────────────────────────────── + + @override + Future getMyPrograms(HistoryRequest request) => + _ok(>[ + { + 'id': 'program-base', + 'name': 'Base strength · 8 weeks', + 'weeks': 8, + 'sessionsPerWeek': 3, + 'deloadWeeks': [4, 8], + 'weeklyContactCeiling': 120, + 'lowerBodyRecoveryHours': 48, + 'active': true, + }, + ]); + + @override + Future saveProgramEntry(Map request) => _done(); + + @override + Future activateProgramEntry(IdRequest request) => _done(); + + @override + Future getProgramSessions(IdRequest request) => + _ok(>[ + { + 'id': 'session-a', + 'programId': 'program-base', + 'dayIndex': 0, + 'name': 'Day A · Squat & press', + 'highIntensityLowerBody': true, + 'prescriptions': [{}, {}, {}, {}], + }, + { + 'id': 'session-b', + 'programId': 'program-base', + 'dayIndex': 2, + 'name': 'Day B · Pull & hinge', + 'highIntensityLowerBody': false, + 'prescriptions': [{}, {}, {}], + }, + ]); + + @override + Future startSessionEntry(IdRequest request) => _done(); + + @override + Future saveSessionLogEntry(SessionLogRequest request) => _done(); + + @override + Future getSessionHistory(HistoryRequest request) => _emptyPage(); + + @override + Future getLastSessionFor(IdRequest request) => _ok({}); + + @override + Future getWeeklyVolume(ReportCardRequest request) => _ok({}); + + @override + Future getContactVolume(ReportCardRequest request) => _ok({}); + + @override + Future getPersonalRecords(HistoryRequest request) => _emptyPage(); + + // ── Report card ─────────────────────────────────────────────────────────── + + @override + Future getWeeklyReportCard(ReportCardRequest request) => _ok(_report()); + + @override + Future getMonthlyReportCard(ReportCardRequest request) => _ok(_report()); + + Map _report() { + final DateTime now = DateTime.now(); + return { + 'id': 'report-week', + 'periodStart': now.subtract(const Duration(days: 7)).toIso8601String(), + 'periodEnd': now.toIso8601String(), + 'completionByClass': { + 'NonNegotiable': 0.92, + 'Standard': 0.71, + 'Elective': 0.40, + }, + 'completionByCategory': { + 'Deep work': 0.64, + 'Admin': 0.88, + 'Health': 0.79, + }, + 'completionByWeekday': { + '1': 0.9, '2': 0.8, '3': 0.75, '4': 0.45, '5': 0.7, '6': 0.6, '7': 0.85, + }, + 'worstHour': 19, + 'debtTrend': [8, 9.5, 11, 10.2, 7.4, 6.8, 5.9], + 'deferralLeaderboard': >[ + {'commitmentId': 'c-email', 'title': 'Email supervisor the outline', 'count': 2}, + {'commitmentId': 'c-kitchen', 'title': 'Deep clean the kitchen', 'count': 1}, + ], + 'excuseTaxonomy': [], + 'estimationAccuracy': {'Deep work': 1.6, 'Admin': 1.1}, + 'trainingAdherence': 0.83, + 'programIntegrity': 0.9, + 'assignedAction': + 'Move deep work before noon on Thursdays. That is where the week leaks.', + 'grade': 'B', + 'praise': 'Every non-negotiable but one landed inside its window.', + }; + } + + // ── Notifications ───────────────────────────────────────────────────────── + + @override + Future getMyNotifications(HistoryRequest request) => _emptyPage(); + + @override + Future acknowledgeNudgeEntry(IdRequest request) => _done(); +} diff --git a/frontend/lib/Grounded/mock/MockMemory.dart b/frontend/lib/Grounded/mock/MockMemory.dart new file mode 100644 index 0000000..c48369c --- /dev/null +++ b/frontend/lib/Grounded/mock/MockMemory.dart @@ -0,0 +1,26 @@ +// MOCK DATA — TEMPORARY. See MockSwitch.dart. + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../about/internal/application/MeDescription.dart'; +import '../about/internal/application/UserDetails.dart'; +import '../memory/InternalMemory.dart'; +import 'MockStorage.dart'; + +/// The real [InternalMemory], pointed at in-memory storage and pre-seeded +/// with a signed-in user so the app opens straight onto Home. +class MockMemory extends InternalMemory { + static bool _seeded = false; + + MockMemory() { + if (!_seeded) { + _seeded = true; + setMyDescription( + MeDescription(id: "mock-user", name: "Alex", token: "mock-token")); + setUserDetails(UserDetails(pic: "", name: "Alex")); + } + } + + @override + FlutterSecureStorage get groundedStorage => const MockStorage(); +} diff --git a/frontend/lib/Grounded/mock/MockStorage.dart b/frontend/lib/Grounded/mock/MockStorage.dart new file mode 100644 index 0000000..61cc152 --- /dev/null +++ b/frontend/lib/Grounded/mock/MockStorage.dart @@ -0,0 +1,55 @@ +// MOCK DATA — TEMPORARY. See MockSwitch.dart. + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// Secure storage held in memory, so mock mode never touches the keychain or +/// browser storage. Seeded as signed in. +class MockStorage extends FlutterSecureStorage { + static final Map _values = {}; + + const MockStorage(); + + @override + Future read({ + required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async { + return _values[key]; + } + + @override + Future write({ + required String key, + required String? value, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async { + if (value == null) { + _values.remove(key); + return; + } + _values[key] = value; + } + + @override + Future delete({ + required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async { + _values.remove(key); + } +} diff --git a/frontend/lib/Grounded/mock/MockStore.dart b/frontend/lib/Grounded/mock/MockStore.dart new file mode 100644 index 0000000..3c8eaef --- /dev/null +++ b/frontend/lib/Grounded/mock/MockStore.dart @@ -0,0 +1,298 @@ +// MOCK DATA — TEMPORARY. See MockSwitch.dart. + +import '../about/external/data/RoutineChain.dart'; +import '../about/internal/application/CommitmentClass.dart'; +import '../utils/RoutineTemplates.dart'; + +/// The fake backend's database: plain JSON maps, seeded relative to today, +/// mutated by [MockComms] so taps actually change what you see. Static, so +/// every screen shares one copy for the life of the app. +class MockStore { + static bool _seeded = false; + + static int _nextId = 100; + + static final List> goals = >[]; + + static final List> commitments = + >[]; + + static final List routines = []; + + static String newId(String prefix) { + _nextId = _nextId + 1; + return "$prefix-$_nextId"; + } + + static void ensureSeeded() { + if (_seeded) { + return; + } + _seeded = true; + + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final int weekday = now.weekday; + + DateTime at(int dayOffset, int hour, [int minute = 0]) { + return today.add(Duration(days: dayOffset, hours: hour, minutes: minute)); + } + + // ── Goals ────────────────────────────────────────────────────────────── + goals.addAll(>[ + { + 'id': 'goal-strong', + 'title': 'Get strong', + 'description': 'Deadlift my bodyweight by spring, without getting hurt.', + 'category': 'Health', + 'defaultClass': 'Standard', + 'startDate': at(-30, 0).toIso8601String(), + 'targetDate': at(60, 0).toIso8601String(), + 'colourHex': '#FF2F6F4E', + 'archived': false, + }, + { + 'id': 'goal-thesis', + 'title': 'Finish the thesis', + 'description': 'Submit before the department deadline. No extensions.', + 'category': 'Work', + 'defaultClass': 'NonNegotiable', + 'startDate': at(-60, 0).toIso8601String(), + 'targetDate': at(45, 0).toIso8601String(), + 'colourHex': '#FF2C4A63', + 'archived': false, + }, + { + 'id': 'goal-calm', + 'title': 'Calm mornings', + 'description': 'Start the day on purpose instead of on the phone.', + 'category': 'Mind', + 'defaultClass': 'Elective', + 'startDate': at(-10, 0).toIso8601String(), + 'targetDate': null, + 'colourHex': '#FF4F5D86', + 'archived': false, + }, + ]); + + // ── Commitments ──────────────────────────────────────────────────────── + Map task( + String id, + String title, { + String? goal, + String cls = 'Standard', + required DateTime start, + required DateTime end, + int minutes = 30, + String proof = 'Honour', + int timer = 0, + String status = 'Open', + int deferrals = 0, + String category = '', + }) { + return { + 'id': id, + 'type': 'TASK', + 'commitmentClass': cls, + 'title': title, + 'category': category, + 'dueStart': start.toIso8601String(), + 'dueEnd': end.toIso8601String(), + 'estMinutes': minutes, + 'energy': 'Medium', + 'proofType': proof, + 'proofTimerMinutes': timer, + 'rrule': '', + 'parentId': goal, + 'status': status, + 'deferralCount': deferrals, + }; + } + + commitments.addAll(>[ + task('c-walk', 'Morning walk, no phone', + goal: 'goal-calm', + cls: 'Elective', + start: at(0, 6, 30), + end: at(0, 8), + minutes: 20, + status: 'Completed'), + task('c-chapter', 'Write 500 words of chapter 3', + goal: 'goal-thesis', + cls: 'NonNegotiable', + start: at(0, 9), + end: at(0, 23, 30), + minutes: 25, + proof: 'Timer', + timer: 1, + category: 'Deep work'), + task('c-rent', 'Pay rent', + cls: 'NonNegotiable', + start: at(0, 8), + end: at(0, 23, 59), + minutes: 5, + category: 'Admin'), + task('c-physio', 'Book physio appointment', + goal: 'goal-strong', + start: at(0, 12), + end: at(0, 23), + minutes: 10, + category: 'Admin'), + task('c-read', 'Read two papers for the literature review', + goal: 'goal-thesis', + start: at(0, 18), + end: at(0, 23, 45), + minutes: 60, + category: 'Deep work'), + task('c-meal', 'Meal prep for the week', + goal: 'goal-strong', + start: at(1, 17), + end: at(1, 20), + minutes: 90), + // Two things already past their window, so the overdue queue and debt + // have something to show. + task('c-email', 'Email supervisor the outline', + goal: 'goal-thesis', + start: at(-2, 9), + end: at(-2, 12), + minutes: 15, + status: 'Overdue', + deferrals: 1), + task('c-kitchen', 'Deep clean the kitchen', + start: at(-1, 18), + end: at(-1, 21), + minutes: 45, + status: 'Overdue'), + ]); + + // ── Routines ─────────────────────────────────────────────────────────── + final RoutineChain strength = RoutineTemplates.fullBody() + ..id = 'routine-strength' + ..goalId = 'goal-strong' + ..days = _withToday([1, 3, 5], weekday) + ..startMinute = 7 * 60; + + final RoutineChain push = RoutineTemplates.upperPush() + ..id = 'routine-push' + ..goalId = 'goal-strong' + ..days = [1, 2, 3, 4, 5, 6, 7].where((day) => day != weekday).take(2).toList(); + + final RoutineChain breathing = RoutineTemplates.boxBreathing() + ..id = 'routine-breathing' + ..goalId = 'goal-calm'; + + final RoutineChain morning = RoutineTemplates.morning() + ..id = 'routine-morning' + ..goalId = 'goal-calm' + ..days = _withToday([1, 2, 3, 4, 5], weekday); + for (int index = 0; index < morning.steps.length; index++) { + morning.steps[index].completed = true; + } + + final RoutineChain shutdown = RoutineTemplates.shutdown() + ..id = 'routine-shutdown' + ..days = _withToday([1, 2, 3, 4, 5], weekday) + ..commitmentClass = CommitmentClass.Standard; + shutdown.steps.first.completed = true; + + final RoutineChain mobility = RoutineTemplates.mobility() + ..id = 'routine-mobility'; + + routines.addAll([ + breathing, + morning, + strength, + shutdown, + push, + mobility, + ]); + } + + static List _withToday(List days, int weekday) { + final List result = List.from(days); + if (!result.contains(weekday)) { + result.add(weekday); + } + result.sort(); + return result; + } + + // ── Derived views ────────────────────────────────────────────────────── + + static bool _settled(Map item) { + final String status = item['status']; + return status == 'Completed' || + status == 'LateCompleted' || + status == 'Abandoned' || + status == 'Archived'; + } + + static bool _windowClosed(Map item) { + final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? ''); + return end != null && end.isBefore(DateTime.now()); + } + + static List> todayPlan() { + final DateTime now = DateTime.now(); + return commitments.where((item) { + final DateTime? start = DateTime.tryParse(item['dueStart'] ?? ''); + return start != null && + start.year == now.year && + start.month == now.month && + start.day == now.day && + item['status'] != 'Abandoned'; + }).toList() + ..sort((a, b) => (a['dueStart'] as String).compareTo(b['dueStart'])); + } + + static List> overdue() { + return commitments + .where((item) => !_settled(item) && _windowClosed(item)) + .map((item) => Map.from(item)..['status'] = 'Overdue') + .toList(); + } + + static List> goalTasks(String goalId) { + return commitments.where((item) => item['parentId'] == goalId).toList(); + } + + /// Goals with the server-derived counts filled in from the tasks. + static List> goalsWithCounts() { + return goals.where((goal) => goal['archived'] != true).map((goal) { + final List> tasks = goalTasks(goal['id']); + final int completed = tasks + .where((item) => + item['status'] == 'Completed' || item['status'] == 'LateCompleted') + .length; + final int late = + tasks.where((item) => !_settled(item) && _windowClosed(item)).length; + return Map.from(goal) + ..['totalTasks'] = tasks.where((item) => item['status'] != 'Abandoned').length + ..['completedTasks'] = completed + ..['overdueTasks'] = late + ..['debtContribution'] = late * 2.0; + }).toList(); + } + + static Map? commitment(String id) { + for (Map item in commitments) { + if (item['id'] == id) { + return item; + } + } + return null; + } + + static Map page(List> content) { + return { + 'number': 0, + 'size': content.length, + 'totalElements': content.length, + 'totalPages': 1, + 'numberOfElements': content.length, + 'first': true, + 'last': true, + 'content': content, + }; + } +} diff --git a/frontend/lib/Grounded/mock/MockSwitch.dart b/frontend/lib/Grounded/mock/MockSwitch.dart new file mode 100644 index 0000000..c0e79a5 --- /dev/null +++ b/frontend/lib/Grounded/mock/MockSwitch.dart @@ -0,0 +1,8 @@ +// ───────────────────────────────────────────────────────────────────────────── +// MOCK DATA — TEMPORARY. Delete the whole `lib/Grounded/mock/` folder and the +// lines tagged `MOCK:` (search the project for "MOCK:") once the backend is up. +// ───────────────────────────────────────────────────────────────────────────── + +/// True runs the app entirely on seeded, in-memory data: no backend, no +/// device storage, no sign-in. Everything resets on restart. +const bool useMockData = true; diff --git a/frontend/lib/Grounded/see/parent/ParentViewModel.dart b/frontend/lib/Grounded/see/parent/ParentViewModel.dart index 93288b5..7c74a5a 100644 --- a/frontend/lib/Grounded/see/parent/ParentViewModel.dart +++ b/frontend/lib/Grounded/see/parent/ParentViewModel.dart @@ -18,6 +18,9 @@ import '../../designs/text/Text.dart'; import '../../informatics/AppDataManager.dart'; import '../../informatics/DataManager.dart'; import '../../memory/InternalMemory.dart'; +import '../../mock/MockComms.dart'; // MOCK: remove with lib/Grounded/mock/ +import '../../mock/MockMemory.dart'; // MOCK: remove with lib/Grounded/mock/ +import '../../mock/MockSwitch.dart'; // MOCK: remove with lib/Grounded/mock/ import '../../utils/Colors.dart'; import '../system/sessionexpired/SessionExpired.dart'; import '../system/updateme/UpdateMe.dart'; @@ -41,7 +44,11 @@ class ParentViewModel extends ChangeNotifier { ParentViewModel(this.context) { overlayState = Overlay.of(context); dataManager = - AppDataManager(InternalMemory(), Comms(InternalMemory()), FileStorage()); + // MOCK: restore to the single real line below when removing mocks. + useMockData + ? AppDataManager(MockMemory(), MockComms(), FileStorage()) + : AppDataManager( + InternalMemory(), Comms(InternalMemory()), FileStorage()); } DataManager getDataManager() { @@ -199,6 +206,11 @@ class ParentViewModel extends ChangeNotifier { Future hasNetwork(Function() actions) async { closeLoading(); + // MOCK: mock mode never needs a connection. + if (useMockData) { + return true; + } + List resultList = await (Connectivity().checkConnectivity());