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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
import '../about/external/data/Commitment.dart';
|
import '../about/external/data/Commitment.dart';
|
||||||
@@ -32,7 +33,7 @@ class LocalNotificationEngine {
|
|||||||
static bool _ready = false;
|
static bool _ready = false;
|
||||||
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
if (_ready) {
|
if (_ready || kIsWeb) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +106,10 @@ class LocalNotificationEngine {
|
|||||||
/// The ongoing notification for a running task. Shows the live remaining
|
/// The ongoing notification for a running task. Shows the live remaining
|
||||||
/// time so it is useful from the lock screen without unlocking.
|
/// time so it is useful from the lock screen without unlocking.
|
||||||
static Future<void> showSessionNotification(LiveSession session) async {
|
static Future<void> showSessionNotification(LiveSession session) async {
|
||||||
|
// The browser has no notification channels; the runner works without.
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
final String remaining = session.requiredSeconds > 0
|
final String remaining = session.requiredSeconds > 0
|
||||||
@@ -144,6 +149,10 @@ class LocalNotificationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> cancelSessionNotification() async {
|
static Future<void> cancelSessionNotification() async {
|
||||||
|
// The browser has no notification channels; the runner works without.
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await init();
|
await init();
|
||||||
await plugin.cancel(id: sessionNotificationId);
|
await plugin.cancel(id: sessionNotificationId);
|
||||||
}
|
}
|
||||||
@@ -156,6 +165,9 @@ class LocalNotificationEngine {
|
|||||||
EscalationTier tier,
|
EscalationTier tier,
|
||||||
ToneLevel tone,
|
ToneLevel tone,
|
||||||
) async {
|
) async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
final bool nonNegotiable =
|
final bool nonNegotiable =
|
||||||
@@ -198,6 +210,10 @@ class LocalNotificationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> cancelAll() async {
|
static Future<void> cancelAll() async {
|
||||||
|
// The browser has no notification channels; the runner works without.
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await init();
|
await init();
|
||||||
await plugin.cancelAll();
|
await plugin.cancelAll();
|
||||||
}
|
}
|
||||||
|
|||||||
443
frontend/lib/Grounded/mock/MockComms.dart
Normal file
443
frontend/lib/Grounded/mock/MockComms.dart
Normal file
@@ -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<Response> _ok(dynamic data) async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 250));
|
||||||
|
return Response(
|
||||||
|
requestOptions: RequestOptions(path: 'mock'),
|
||||||
|
statusCode: 200,
|
||||||
|
data: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> _done() => _ok(<String, dynamic>{'success': true});
|
||||||
|
|
||||||
|
Future<Response> _emptyPage() => _ok(MockStore.page(<Map<String, dynamic>>[]));
|
||||||
|
|
||||||
|
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> registerDevice(DeviceRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> registerDeviceToken(String token) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> login(LoginData request) => _ok(<String, dynamic>{
|
||||||
|
'id': 'mock-user',
|
||||||
|
'name': 'Alex',
|
||||||
|
'token': 'mock-token',
|
||||||
|
'pic': '',
|
||||||
|
'tone': 'Firm',
|
||||||
|
'standing': 'Good',
|
||||||
|
'amnestyTokens': 3,
|
||||||
|
'sickMode': false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> logout() => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> me() => _ok(<String, dynamic>{'id': 'mock-user', 'name': 'Alex'});
|
||||||
|
|
||||||
|
// ── Goals ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getMyGoals(HistoryRequest request) =>
|
||||||
|
_ok(MockStore.goalsWithCounts());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveGoalEntry(GoalRequest request) {
|
||||||
|
final Map<String, dynamic> 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<Response> getGoalTasks(IdRequest request) =>
|
||||||
|
_ok(MockStore.goalTasks(request.id));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> archiveGoalEntry(IdRequest request) {
|
||||||
|
for (Map<String, dynamic> goal in MockStore.goals) {
|
||||||
|
if (goal['id'] == request.id) {
|
||||||
|
goal['archived'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _done();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Commitments ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getTodayPlan(CommitmentsRequest request) =>
|
||||||
|
_ok(MockStore.page(MockStore.todayPlan()));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getMyCommitments(CommitmentsRequest request) =>
|
||||||
|
_ok(MockStore.page(MockStore.commitments));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getOverdueQueue(HistoryRequest request) =>
|
||||||
|
_ok(MockStore.page(MockStore.overdue()));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveCommitmentEntry(CommitmentRequest request) {
|
||||||
|
final Map<String, dynamic> json = request.toJson();
|
||||||
|
json['id'] = MockStore.newId('task');
|
||||||
|
json['status'] = 'Open';
|
||||||
|
json['deferralCount'] = 0;
|
||||||
|
MockStore.commitments.add(json);
|
||||||
|
return _ok(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> updateCommitmentEntry(CommitmentRequest request) {
|
||||||
|
final Map<String, dynamic>? item = MockStore.commitment(request.id ?? '');
|
||||||
|
if (item != null) {
|
||||||
|
item.addAll(request.toJson());
|
||||||
|
}
|
||||||
|
return _done();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> deleteCommitmentEntry(IdRequest request) {
|
||||||
|
MockStore.commitments.removeWhere((item) => item['id'] == request.id);
|
||||||
|
return _done();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> completeCommitmentEntry(CompletionRequest request) {
|
||||||
|
final Map<String, dynamic>? 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<Response> deferCommitmentEntry(DeferralRequest request) {
|
||||||
|
final Map<String, dynamic>? 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<Response> abandonCommitmentEntry(AbandonRequest request) {
|
||||||
|
final Map<String, dynamic>? item = MockStore.commitment(request.commitmentId);
|
||||||
|
if (item != null) {
|
||||||
|
item['status'] = 'Abandoned';
|
||||||
|
}
|
||||||
|
return _done();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getCommitmentHistory(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getCommitmentEvents(IdRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
// ── Capacity ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> checkCapacity(CommitmentsRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getCapacityProfileEntry() => _ok(<String, dynamic>{});
|
||||||
|
|
||||||
|
// ── Debt & standing ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getDebtSummary() => _ok(<String, dynamic>{});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getDebtLedger(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getDebtTrend(ReportCardRequest request) => _ok(<dynamic>[]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getStanding() => _ok(<String, dynamic>{'standing': 'Good'});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getStandingHistory(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
// ── Excuses ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getExcuseClusters(ReportCardRequest request) =>
|
||||||
|
_ok(<Map<String, dynamic>>[
|
||||||
|
{
|
||||||
|
'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<Response> uploadPhotoProof(FormData request) =>
|
||||||
|
_ok(<String, dynamic>{'proofRef': 'mock-photo'});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> submitTimerProofEntry(CompletionRequest request) =>
|
||||||
|
completeCommitmentEntry(request);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> submitLocationProofEntry(CompletionRequest request) =>
|
||||||
|
completeCommitmentEntry(request);
|
||||||
|
|
||||||
|
// ── Guardrails ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> spendAmnestyToken(AmnestyRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getAmnestyBalance() => _ok(<String, dynamic>{'balance': 3});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> updateSickMode(SickModeRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> checkDistress() => _ok(<String, dynamic>{'distressed': false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> updateTone(ToneRequest request) => _done();
|
||||||
|
|
||||||
|
// ── Habits & routines ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getMyHabits(HistoryRequest request) => _ok(<dynamic>[]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveHabitEntry(Map<String, dynamic> request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> logHabitEntry(IdRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getKeystoneHabits() => _ok(<dynamic>[]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getMyRoutines(HistoryRequest request) => _ok(
|
||||||
|
MockStore.routines.map((routine) => routine.toJson()).toList());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveRoutineEntry(Map<String, dynamic> 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<Response> logRoutineChainEntry(Map<String, dynamic> 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<Response> getMyPrograms(HistoryRequest request) =>
|
||||||
|
_ok(<Map<String, dynamic>>[
|
||||||
|
{
|
||||||
|
'id': 'program-base',
|
||||||
|
'name': 'Base strength · 8 weeks',
|
||||||
|
'weeks': 8,
|
||||||
|
'sessionsPerWeek': 3,
|
||||||
|
'deloadWeeks': <int>[4, 8],
|
||||||
|
'weeklyContactCeiling': 120,
|
||||||
|
'lowerBodyRecoveryHours': 48,
|
||||||
|
'active': true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveProgramEntry(Map<String, dynamic> request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> activateProgramEntry(IdRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getProgramSessions(IdRequest request) =>
|
||||||
|
_ok(<Map<String, dynamic>>[
|
||||||
|
{
|
||||||
|
'id': 'session-a',
|
||||||
|
'programId': 'program-base',
|
||||||
|
'dayIndex': 0,
|
||||||
|
'name': 'Day A · Squat & press',
|
||||||
|
'highIntensityLowerBody': true,
|
||||||
|
'prescriptions': <dynamic>[{}, {}, {}, {}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'session-b',
|
||||||
|
'programId': 'program-base',
|
||||||
|
'dayIndex': 2,
|
||||||
|
'name': 'Day B · Pull & hinge',
|
||||||
|
'highIntensityLowerBody': false,
|
||||||
|
'prescriptions': <dynamic>[{}, {}, {}],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> startSessionEntry(IdRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> saveSessionLogEntry(SessionLogRequest request) => _done();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getSessionHistory(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getLastSessionFor(IdRequest request) => _ok(<String, dynamic>{});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getWeeklyVolume(ReportCardRequest request) => _ok(<String, dynamic>{});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getContactVolume(ReportCardRequest request) => _ok(<String, dynamic>{});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getPersonalRecords(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
// ── Report card ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getWeeklyReportCard(ReportCardRequest request) => _ok(_report());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> getMonthlyReportCard(ReportCardRequest request) => _ok(_report());
|
||||||
|
|
||||||
|
Map<String, dynamic> _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': <double>[8, 9.5, 11, 10.2, 7.4, 6.8, 5.9],
|
||||||
|
'deferralLeaderboard': <Map<String, dynamic>>[
|
||||||
|
{'commitmentId': 'c-email', 'title': 'Email supervisor the outline', 'count': 2},
|
||||||
|
{'commitmentId': 'c-kitchen', 'title': 'Deep clean the kitchen', 'count': 1},
|
||||||
|
],
|
||||||
|
'excuseTaxonomy': <dynamic>[],
|
||||||
|
'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<Response> getMyNotifications(HistoryRequest request) => _emptyPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response> acknowledgeNudgeEntry(IdRequest request) => _done();
|
||||||
|
}
|
||||||
26
frontend/lib/Grounded/mock/MockMemory.dart
Normal file
26
frontend/lib/Grounded/mock/MockMemory.dart
Normal file
@@ -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();
|
||||||
|
}
|
||||||
55
frontend/lib/Grounded/mock/MockStorage.dart
Normal file
55
frontend/lib/Grounded/mock/MockStorage.dart
Normal file
@@ -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<String, String> _values = <String, String>{};
|
||||||
|
|
||||||
|
const MockStorage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read({
|
||||||
|
required String key,
|
||||||
|
AppleOptions? iOptions,
|
||||||
|
AndroidOptions? aOptions,
|
||||||
|
LinuxOptions? lOptions,
|
||||||
|
WebOptions? webOptions,
|
||||||
|
AppleOptions? mOptions,
|
||||||
|
WindowsOptions? wOptions,
|
||||||
|
}) async {
|
||||||
|
return _values[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> 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<void> delete({
|
||||||
|
required String key,
|
||||||
|
AppleOptions? iOptions,
|
||||||
|
AndroidOptions? aOptions,
|
||||||
|
LinuxOptions? lOptions,
|
||||||
|
WebOptions? webOptions,
|
||||||
|
AppleOptions? mOptions,
|
||||||
|
WindowsOptions? wOptions,
|
||||||
|
}) async {
|
||||||
|
_values.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
298
frontend/lib/Grounded/mock/MockStore.dart
Normal file
298
frontend/lib/Grounded/mock/MockStore.dart
Normal file
@@ -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<Map<String, dynamic>> goals = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
static final List<Map<String, dynamic>> commitments =
|
||||||
|
<Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
static final List<RoutineChain> routines = <RoutineChain>[];
|
||||||
|
|
||||||
|
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(<Map<String, dynamic>>[
|
||||||
|
{
|
||||||
|
'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<String, dynamic> 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(<Map<String, dynamic>>[
|
||||||
|
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(<int>[1, 3, 5], weekday)
|
||||||
|
..startMinute = 7 * 60;
|
||||||
|
|
||||||
|
final RoutineChain push = RoutineTemplates.upperPush()
|
||||||
|
..id = 'routine-push'
|
||||||
|
..goalId = 'goal-strong'
|
||||||
|
..days = <int>[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(<int>[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(<int>[1, 2, 3, 4, 5], weekday)
|
||||||
|
..commitmentClass = CommitmentClass.Standard;
|
||||||
|
shutdown.steps.first.completed = true;
|
||||||
|
|
||||||
|
final RoutineChain mobility = RoutineTemplates.mobility()
|
||||||
|
..id = 'routine-mobility';
|
||||||
|
|
||||||
|
routines.addAll(<RoutineChain>[
|
||||||
|
breathing,
|
||||||
|
morning,
|
||||||
|
strength,
|
||||||
|
shutdown,
|
||||||
|
push,
|
||||||
|
mobility,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> _withToday(List<int> days, int weekday) {
|
||||||
|
final List<int> result = List<int>.from(days);
|
||||||
|
if (!result.contains(weekday)) {
|
||||||
|
result.add(weekday);
|
||||||
|
}
|
||||||
|
result.sort();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Derived views ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static bool _settled(Map<String, dynamic> item) {
|
||||||
|
final String status = item['status'];
|
||||||
|
return status == 'Completed' ||
|
||||||
|
status == 'LateCompleted' ||
|
||||||
|
status == 'Abandoned' ||
|
||||||
|
status == 'Archived';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _windowClosed(Map<String, dynamic> item) {
|
||||||
|
final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? '');
|
||||||
|
return end != null && end.isBefore(DateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Map<String, dynamic>> 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<Map<String, dynamic>> overdue() {
|
||||||
|
return commitments
|
||||||
|
.where((item) => !_settled(item) && _windowClosed(item))
|
||||||
|
.map((item) => Map<String, dynamic>.from(item)..['status'] = 'Overdue')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Map<String, dynamic>> goalTasks(String goalId) {
|
||||||
|
return commitments.where((item) => item['parentId'] == goalId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Goals with the server-derived counts filled in from the tasks.
|
||||||
|
static List<Map<String, dynamic>> goalsWithCounts() {
|
||||||
|
return goals.where((goal) => goal['archived'] != true).map((goal) {
|
||||||
|
final List<Map<String, dynamic>> 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<String, dynamic>.from(goal)
|
||||||
|
..['totalTasks'] = tasks.where((item) => item['status'] != 'Abandoned').length
|
||||||
|
..['completedTasks'] = completed
|
||||||
|
..['overdueTasks'] = late
|
||||||
|
..['debtContribution'] = late * 2.0;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic>? commitment(String id) {
|
||||||
|
for (Map<String, dynamic> item in commitments) {
|
||||||
|
if (item['id'] == id) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> page(List<Map<String, dynamic>> content) {
|
||||||
|
return {
|
||||||
|
'number': 0,
|
||||||
|
'size': content.length,
|
||||||
|
'totalElements': content.length,
|
||||||
|
'totalPages': 1,
|
||||||
|
'numberOfElements': content.length,
|
||||||
|
'first': true,
|
||||||
|
'last': true,
|
||||||
|
'content': content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/lib/Grounded/mock/MockSwitch.dart
Normal file
8
frontend/lib/Grounded/mock/MockSwitch.dart
Normal file
@@ -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;
|
||||||
@@ -18,6 +18,9 @@ import '../../designs/text/Text.dart';
|
|||||||
import '../../informatics/AppDataManager.dart';
|
import '../../informatics/AppDataManager.dart';
|
||||||
import '../../informatics/DataManager.dart';
|
import '../../informatics/DataManager.dart';
|
||||||
import '../../memory/InternalMemory.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 '../../utils/Colors.dart';
|
||||||
import '../system/sessionexpired/SessionExpired.dart';
|
import '../system/sessionexpired/SessionExpired.dart';
|
||||||
import '../system/updateme/UpdateMe.dart';
|
import '../system/updateme/UpdateMe.dart';
|
||||||
@@ -41,7 +44,11 @@ class ParentViewModel extends ChangeNotifier {
|
|||||||
ParentViewModel(this.context) {
|
ParentViewModel(this.context) {
|
||||||
overlayState = Overlay.of(context);
|
overlayState = Overlay.of(context);
|
||||||
dataManager =
|
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() {
|
DataManager getDataManager() {
|
||||||
@@ -199,6 +206,11 @@ class ParentViewModel extends ChangeNotifier {
|
|||||||
Future<bool> hasNetwork(Function() actions) async {
|
Future<bool> hasNetwork(Function() actions) async {
|
||||||
closeLoading();
|
closeLoading();
|
||||||
|
|
||||||
|
// MOCK: mock mode never needs a connection.
|
||||||
|
if (useMockData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
List<ConnectivityResult> resultList =
|
List<ConnectivityResult> resultList =
|
||||||
await (Connectivity().checkConnectivity());
|
await (Connectivity().checkConnectivity());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user