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:
alvocool
2026-11-02 08:45:41 +03:00
parent 1a5867a98f
commit b31bc47cc0
7 changed files with 860 additions and 2 deletions

View File

@@ -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<void> 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<void> 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<void> 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<void> cancelAll() async {
// The browser has no notification channels; the runner works without.
if (kIsWeb) {
return;
}
await init();
await plugin.cancelAll();
}

View 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();
}

View 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();
}

View 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);
}
}

View 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,
};
}
}

View 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;

View File

@@ -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<bool> hasNetwork(Function() actions) async {
closeLoading();
// MOCK: mock mode never needs a connection.
if (useMockData) {
return true;
}
List<ConnectivityResult> resultList =
await (Connectivity().checkConnectivity());