import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/data/Goal.dart'; import '../../about/external/initial/GoalRequest.dart'; import '../../about/internal/application/CommitmentClass.dart'; import '../../about/internal/application/NotificationType.dart'; import '../../about/internal/application/TextType.dart'; import '../../designs/Component.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/input/InputFields.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/CommonUtils.dart'; import '../../utils/Validators.dart'; import 'ConnectGoalEditor.dart'; import 'GoalEditor.dart'; import 'ViewGoalEditor.dart'; class GoalEditorState extends State implements ConnectGoalEditor { ViewGoalEditor? _model; final GlobalKey _formKey = GlobalKey(); final TextEditingController _title = TextEditingController(); final TextEditingController _description = TextEditingController(); final TextEditingController _category = TextEditingController(); CommitmentClass _defaultClass = CommitmentClass.Standard; Color _accent = goalAccents.first; DateTime? _targetDate; bool get _editing { return widget.goal != null; } @override void initState() { super.initState(); final goal = widget.goal; if (goal != null) { _title.text = goal.title; _description.text = goal.description; _category.text = goal.category; _defaultClass = goal.defaultClass; _targetDate = goal.targetDate; if (goal.colourHex.isNotEmpty) { _accent = goalColor(goal.colourHex); } } } @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGoalEditor(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() {} // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context, false); } void _onTitleChanged(String value) { setState(() {}); } void _onAccentSelected(Color colour) { setState(() { _accent = colour; }); } void _onClassSelected(CommitmentClass value) { setState(() { _defaultClass = value; }); } void _onPickTarget() async { final DateTime now = DateTime.now(); final DateTime? picked = await showDatePicker( context: context, initialDate: _targetDate ?? now.add(const Duration(days: 30)), firstDate: now, lastDate: now.add(const Duration(days: 365 * 5)), ); if (picked == null) { return; } setState(() { _targetDate = picked; }); } void _onClearTarget() { setState(() { _targetDate = null; }); } void _onSave() { if (_formKey.currentState?.validate() != true) { return; } _model?.save(GoalRequest( id: widget.goal?.id, title: _title.text.trim(), description: _description.text.trim(), category: _category.text.trim(), defaultClass: _defaultClass.name, startDate: (widget.goal?.startDate ?? DateTime.now()).toIso8601String(), targetDate: _targetDate?.toIso8601String() ?? "", colourHex: colorToHex(_accent), )); } void _onArchive() { _model?.showApplicationNotification( NotificationType.warning, "Archive this goal?", "It leaves your goals list. Its tasks and their history stay on the record, and any debt they carry is not forgiven.", true, true, null, action: "Archive", positiveAction: _onConfirmArchive, ); } void _onConfirmArchive() { Navigator.pop(context); _model?.archive(widget.goal?.id ?? ""); } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: _editing ? "Edit" : "New", title: "Goal", onBack: _onBack, child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ displayTitle(_editing ? "Adjust the goal." : "What are you\nworking toward?"), const SizedBox(height: 24), _preview(), const SizedBox(height: 28), inputField( "Goal", _title, hint: "Workout, thesis, get the flat sorted…", validator: Validators.title, onChanged: _onTitleChanged, ), const SizedBox(height: 16), inputField( "Why it matters", _description, hint: "Optional, but it helps on the bad days", maxLines: 3, ), const SizedBox(height: 16), inputField( "Category", _category, hint: "Health, work, home…", ), const SizedBox(height: 28), sectionBreak("Colour", caption: "how it reads everywhere"), _swatches(), const SizedBox(height: 28), sectionBreak("Deadline", caption: "optional"), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: selectField( "Target date", _targetDate == null ? "" : formatDate(_targetDate), _onPickTarget, hint: "No deadline", icon: CupertinoIcons.calendar, ), ), if (_targetDate != null) ...[ const SizedBox(width: 8), iconButton( Icon(CupertinoIcons.xmark, size: 15, color: colorGrey2), _onClearTarget, bordered: true, background: colorWhite, radius: 12, size: 50, ), ], ], ), const SizedBox(height: 10), text( "A deadline is for the goal as a whole. The tasks and routines inside it keep their own windows.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), const SizedBox(height: 28), sectionBreak("Tasks default to", caption: "you can change each one"), segmentedSelector( options: CommitmentClass.values, selected: _defaultClass, label: classLabel, onSelected: _onClassSelected, activeColor: classColor(_defaultClass), ), const SizedBox(height: 10), text( "The goal never carries debt itself — the tasks and routines inside it do.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), const SizedBox(height: 32), roundedCornerButton( _editing ? "Save changes" : "Create goal", _onSave, icon: _editing ? CupertinoIcons.checkmark : CupertinoIcons.add, ), if (_editing) ...[ const SizedBox(height: 8), Center( child: textButton("Archive goal", _onArchive, color: colorDestructive, textSize: 13), ), ], ], ), ), ); } /// A live preview of the goal card, so colour and deadline are chosen by /// looking, not imagining. Widget _preview() { final String title = _title.text.trim().isEmpty ? "Your goal" : _title.text.trim(); return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( borderRadius: BorderRadius.zero, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_accent, Color.lerp(_accent, colorBlack, 0.45)!], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Icon(CupertinoIcons.flag_fill, size: 14, color: colorWhite.withValues(alpha: 0.7)), const SizedBox(width: 8), text( _targetDate == null ? "NO DEADLINE" : targetLabel(_targetDate).toUpperCase(), 9, TextType.Bold, color: colorWhite.withValues(alpha: 0.6), letterSpacing: 1.2, ), ], ), const SizedBox(height: 14), text(title, 24, TextType.Light, color: colorWhite, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 16), meter(_editing ? widget.goal!.progress : 0, fill: colorWhite, track: colorWhite.withValues(alpha: 0.16), height: 5), ], ), ); } Widget _swatches() { return Wrap( spacing: 9, runSpacing: 9, children: goalAccents.map((colour) { final bool selected = colour.toARGB32() == _accent.toARGB32(); return GestureDetector( onTap: () => _onAccentSelected(colour), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 36, height: 36, alignment: Alignment.center, decoration: BoxDecoration( color: colour, border: Border.all( color: selected ? colorWhite : Colors.transparent, width: 3, ), boxShadow: selected ? [BoxShadow(color: colour, spreadRadius: 1.5)] : null, ), child: selected ? Icon(Icons.check_rounded, size: 18, color: colorWhite) : null, ), ); }).toList(), ); } // ── ConnectGoalEditor ───────────────────────────────────────────────────── /// Pops the goal as saved, carrying over the server-derived counts, so the /// screen underneath can update in place without a reload. @override void onSaved() { final Goal? before = widget.goal; Navigator.pop( context, Goal( id: before?.id, title: _title.text.trim(), description: _description.text.trim(), category: _category.text.trim(), defaultClass: _defaultClass, startDate: before?.startDate ?? DateTime.now(), targetDate: _targetDate, colourHex: colorToHex(_accent), totalTasks: before?.totalTasks ?? 0, completedTasks: before?.completedTasks ?? 0, overdueTasks: before?.overdueTasks ?? 0, debtContribution: before?.debtContribution ?? 0, ), ); } @override void onArchived() { Navigator.pop(context, "archived"); } @override void dispose() { _title.dispose(); _description.dispose(); _category.dispose(); super.dispose(); } }