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

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