import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/Commitment.dart'; import '../../about/external/data/Goal.dart'; import '../../about/external/data/RoutineChain.dart'; import '../../about/internal/application/CommitmentStatus.dart'; import '../../about/internal/application/NotificationType.dart'; import '../../about/internal/application/ProofType.dart'; import '../../about/internal/application/RoutineKind.dart'; import '../../about/internal/application/TextType.dart'; import '../../configs/Navigator.dart'; import '../../designs/Checkbox.dart'; import '../../designs/Component.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/CommonUtils.dart'; import '../../utils/DebtEngine.dart'; import '../commitment/NewCommitment.dart'; import '../live/LiveTask.dart'; import '../routine/RoutineBuilder.dart'; import '../routine/RoutineRunner.dart'; import 'ConnectGoalDetail.dart'; import 'GoalDetail.dart'; import 'GoalEditor.dart'; import 'ViewGoalDetail.dart'; class GoalDetailState extends State implements ConnectGoalDetail { ViewGoalDetail? _model; Goal _goal = Goal(); List _tasks = []; List _routines = []; bool _changed = false; /// Ticked locally, awaiting the server. final Set _ticked = {}; @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGoalDetail(context, this), onViewModelReady: (viewModel) { _model = viewModel; _initiate(); }, builder: (context, viewModel, child) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Responsive( mobile: _mobileView(constraints), tablet: _mobileView(constraints), desktop: _mobileView(constraints), ); }, ), ); } void _initiate() { setState(() { _goal = widget.goal; }); _model?.loadTasks(widget.goal); } // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context, _changed); } void _onStartTask(Commitment task) { _model?.startTask(task); } /// Ticking completes the task without opening the runner — the runner is for /// work you actually want timed. void _onTick(Commitment task) { if (task.id == null || _ticked.contains(task.id)) { return; } setState(() { _ticked.add(task.id!); }); _model?.tick(task); } void _onAddTask() async { final result = await GroundedNavigation() .navigateToPageWithData(NewCommitment(goal: _goal), context); if (result == true) { _changed = true; _model?.loadTasks(_goal); } } void _onAddRoutine() async { final result = await GroundedNavigation() .navigateToPageWithData(RoutineBuilder(goal: _goal), context); if (result == true) { _model?.loadRoutines(_goal); } } void _onEditRoutine(RoutineChain routine) async { final result = await GroundedNavigation() .navigateToPageWithData( RoutineBuilder(routine: routine, goal: _goal), context); if (result == true) { _model?.loadRoutines(_goal); } } void _onStartRoutine(RoutineChain routine) async { if (routine.steps.isEmpty) { _onEditRoutine(routine); return; } final result = await GroundedNavigation().navigateToPageWithData( RoutineRunner(routine: routine.copy()), context); if (result == true) { _changed = true; _model?.loadRoutines(_goal); } } void _onEditGoal() async { final result = await GroundedNavigation() .navigateToPageWithData(GoalEditor(goal: _goal), context); if (!mounted) { return; } if (result == "archived") { Navigator.pop(context, true); return; } if (result is Goal) { _changed = true; setState(() { _goal = result; }); } } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { final List open = _tasks .where((task) => task.status != CommitmentStatus.Completed && task.status != CommitmentStatus.LateCompleted && task.status != CommitmentStatus.Abandoned) .toList(); final List done = _tasks .where((task) => task.status == CommitmentStatus.Completed || task.status == CommitmentStatus.LateCompleted) .toList(); return Sheet( eyebrow: _goal.category.isEmpty ? "Goal" : _goal.category, title: _goal.title, onBack: _onBack, chrome: _goal.colourHex.isEmpty ? null : goalColor(_goal.colourHex), action: chromeAction(CupertinoIcons.pencil, _onEditGoal), banner: _progressBanner(), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (_goal.description.isNotEmpty) ...[ text(_goal.description, 15, TextType.Regular, color: colorGrey2, height: 1.6), const SizedBox(height: 28), ], sectionBreak("To do", caption: "${open.length} open"), if (open.isEmpty) emptyState( CupertinoIcons.square_list, "Nothing scheduled", "Add the actual sessions — Monday shoulders, Wednesday legs — and they start counting.", ) else ...open.map(_taskRow), const SizedBox(height: 28), sectionBreak( "Routines", caption: _routines.isEmpty ? "none yet" : "${_routines.length} serving this goal", ), if (_routines.isEmpty) text( "Routines are the repeatable part of a goal — the Monday session, the morning sit. Add one and it runs from here.", 13, TextType.Regular, color: colorGrey2, height: 1.5, ) else ..._routines.map(_routineRow), const SizedBox(height: 12), outlinedActionButton("Add a routine", _onAddRoutine, icon: CupertinoIcons.square_stack_3d_up), if (done.isNotEmpty) ...[ const SizedBox(height: 28), sectionBreak("Done", caption: "${done.length}"), ...done.map(_doneRow), ], const SizedBox(height: 24), roundedCornerButton("Add a task", _onAddTask, icon: CupertinoIcons.add), ], ), ); } Widget _progressBanner() { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: colorWhite.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ text("PROGRESS", 9, TextType.Bold, color: colorWhite.withValues(alpha: 0.45), letterSpacing: 1.2), text("${(_goal.progress * 100).round()}%", 13, TextType.Bold, color: colorWhite), ], ), const SizedBox(height: 12), meter( _goal.progress, fill: colorWhite, track: colorWhite.withValues(alpha: 0.14), height: 5, ), const SizedBox(height: 14), Row( children: [ Expanded( child: labelled( "Tasks", "${_goal.completedTasks}/${_goal.totalTasks}", valueSize: 15, valueColor: colorWhite, labelColor: colorWhite.withValues(alpha: 0.45), ), ), Expanded( child: labelled( "Routines", "${_routines.length}", valueSize: 15, valueColor: colorWhite, labelColor: colorWhite.withValues(alpha: 0.45), ), ), Expanded( child: labelled( "Deadline", _goal.targetDate == null ? "None" : targetLabel(_goal.targetDate), valueSize: 15, valueColor: colorWhite, labelColor: colorWhite.withValues(alpha: 0.45), ), ), ], ), ], ), ); } /// A task row leads with the action: the point of opening a goal is to start /// something, not to admire the list. Widget _taskRow(Commitment task) { final Color accent = classColor(task.commitmentClass); final bool late = task.windowClosed; return Container( margin: const EdgeInsets.only(bottom: 10), child: card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ TickRow( checked: _ticked.contains(task.id), title: task.title, accent: accent, onTick: () => _onTick(task), detail: Row( children: [ text(formatWindow(task), 11, TextType.Regular, color: colorGrey2), const SizedBox(width: 9), Container( width: 3, height: 3, decoration: BoxDecoration( color: colorGrey, shape: BoxShape.circle), ), const SizedBox(width: 9), text(formatMinutes(task.estMinutes), 11, TextType.Regular, color: colorGrey2), ], ), trailing: pill(proofLabel(task.proofType), colorGrey2, colorMuted, textSize: 9), ), if (late) ...[ const SizedBox(height: 12), Row( children: [ pill(overdueLabel(task), colorStandingGrounded, colorStandingGroundedBg, textSize: 9), const SizedBox(width: 6), pill("−${formatDebt(DebtEngine.commitmentDebt(task))}", colorGrey2, colorMuted, textSize: 9), ], ), ], const SizedBox(height: 14), SizedBox( width: double.infinity, child: roundedCornerButton( "Start", () => _onStartTask(task), icon: CupertinoIcons.play_fill, verticalPadding: 13, ), ), ], ), ), ); } Widget _doneRow(Commitment task) { final bool late = task.status == CommitmentStatus.LateCompleted; return Container( margin: const EdgeInsets.only(bottom: 8), child: card( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), child: TickRow( checked: true, locked: true, title: task.title, accent: late ? colorStandingWarned : colorPositive, trailing: late ? pill("Late", colorStandingWarned, colorStandingWarnedBg, textSize: 9) : null, ), ), ); } Widget _routineRow(RoutineChain routine) { final Color tone = kindColor(routine.kind); return Container( margin: const EdgeInsets.only(bottom: 10), child: card( padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), onTap: () => _onEditRoutine(routine), child: Row( children: [ Container( width: 48, height: 48, alignment: Alignment.center, decoration: BoxDecoration( color: kindBackground(routine.kind), borderRadius: BorderRadius.circular(13), ), child: Icon(kindIcon(routine.kind), size: 21, color: tone), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ text(routine.title, 14, TextType.Medium, color: colorPrimaryDark, maxLines: 1, overflow: TextOverflow.ellipsis), const SizedBox(height: 4), text( [ kindLabel(routine.kind), formatMinutes(routine.totalSeconds / 60), if (routine.startMinute >= 0) formatStartMinute(routine.startMinute), formatDays(routine.days), ].join(" · "), 11, TextType.Regular, color: colorGrey2, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), GestureDetector( onTap: () => _onStartRoutine(routine), child: Container( width: 40, height: 40, alignment: Alignment.center, decoration: BoxDecoration( color: routine.kind == RoutineKind.MustDo ? colorPrimaryDark : tone, shape: BoxShape.circle, ), child: Icon(CupertinoIcons.play_fill, size: 15, color: colorWhite), ), ), ], ), ), ); } // ── ConnectGoalDetail ───────────────────────────────────────────────────── @override void onRoutinesLoaded(List routines) { setState(() { _routines = routines; }); } @override void onGoalLoaded(Goal goal, List tasks) { setState(() { _ticked.removeWhere((id) => tasks.every((task) => task.id != id)); _goal = goal; _tasks = tasks; }); } @override void onTaskTicked(Commitment task, bool late) { _changed = true; _model?.loadTasks(_goal); if (!late) { return; } _model?.showApplicationNotification( NotificationType.warning, "Late complete", "The window had already closed. This is recorded as a late complete — it reduces the debt but does not clear it.", true, true, null, ); } @override void onTickFailed(Commitment task) { setState(() { _ticked.remove(task.id); }); } @override void onTaskReady(Commitment task) async { // The runner takes over the whole screen — a task you are running is the // thing you are doing, not a row in a list. final result = await GroundedNavigation().navigateToPageWithData( LiveTask(commitment: task, goalTitle: _goal.title), context, ); if (result == true) { _changed = true; _model?.loadTasks(_goal); } } }