import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stacked/stacked.dart'; import '../../about/external/initial/GroupGoalRequest.dart'; import '../../about/external/initial/ObjectiveDraft.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 'ConnectGroupGoalEditor.dart'; import 'GroupGoalEditor.dart'; import 'ViewGroupGoalEditor.dart'; /// The three fields of one objective while it is being typed. typedef _ObjectiveFields = ({ TextEditingController title, TextEditingController target, TextEditingController unit, }); class GroupGoalEditorState extends State implements ConnectGroupGoalEditor { ViewGroupGoalEditor? _model; final GlobalKey _formKey = GlobalKey(); final TextEditingController _title = TextEditingController(); final TextEditingController _description = TextEditingController(); final List<_ObjectiveFields> _objectives = <_ObjectiveFields>[]; DateTime? _targetDate; @override void initState() { super.initState(); _objectives.add(_blankObjective()); } @override void dispose() { _title.dispose(); _description.dispose(); for (_ObjectiveFields fields in _objectives) { fields.title.dispose(); fields.target.dispose(); fields.unit.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( viewModelBuilder: () => ViewGroupGoalEditor(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() {} _ObjectiveFields _blankObjective() { return ( title: TextEditingController(), target: TextEditingController(), unit: TextEditingController(), ); } // ── Handlers ────────────────────────────────────────────────────────────── void _onBack() { Navigator.pop(context, false); } void _onAddObjective() { setState(() { _objectives.add(_blankObjective()); }); } void _onRemoveObjective(_ObjectiveFields fields) { setState(() { _objectives.remove(fields); }); fields.title.dispose(); fields.target.dispose(); fields.unit.dispose(); } 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 * 3)), ); if (picked == null) { return; } setState(() { _targetDate = picked; }); } void _onClearTarget() { setState(() { _targetDate = null; }); } void _onSave() { if (_formKey.currentState?.validate() != true) { return; } _model?.save(GroupGoalRequest( groupId: widget.group.id, title: _title.text.trim(), description: _description.text.trim(), targetDate: _targetDate?.toIso8601String() ?? "", colourHex: widget.group.colourHex, objectives: _objectives .map((fields) => ObjectiveDraft( title: fields.title.text.trim(), unit: fields.unit.text.trim(), target: double.tryParse(fields.target.text.trim()) ?? 0, )) .toList(), )); } String? _validateTarget(String? value) { final double? target = double.tryParse((value ?? "").trim()); if (target == null || target <= 0) { return "A number above 0."; } return null; } String? _validateUnit(String? value) { return Validators.required(value, "Unit"); } // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { return Sheet( eyebrow: widget.group.name, title: "Shared goal", onBack: _onBack, child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ displayTitle("What are you\nall going for?"), const SizedBox(height: 24), inputField( "Goal", _title, hint: "Run 200 km this month, finish the drafts…", validator: Validators.title, ), const SizedBox(height: 16), inputField( "Why it matters", _description, hint: "Optional", maxLines: 2, ), const SizedBox(height: 28), sectionBreak("Objectives", caption: "what everyone logs toward"), text( "Break the goal into numbers people can add to: kilometres, sessions, pages, hours. Each member logs their share and the totals add up.", 12, TextType.Regular, color: colorGrey2, height: 1.5, ), const SizedBox(height: 14), for (int index = 0; index < _objectives.length; index++) _objectiveEditor(index, _objectives[index]), outlinedActionButton("Add objective", _onAddObjective, icon: CupertinoIcons.add), 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, size: 50, ), ], ], ), const SizedBox(height: 32), roundedCornerButton("Share with the group", _onSave, icon: CupertinoIcons.person_3_fill), ], ), ), ); } Widget _objectiveEditor(int index, _ObjectiveFields fields) { return Container( margin: const EdgeInsets.only(bottom: 12), child: card( padding: const EdgeInsets.fromLTRB(16, 12, 8, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ text("OBJECTIVE ${index + 1}", 10, TextType.Bold, color: colorGrey2, letterSpacing: 1.0), const Spacer(), if (_objectives.length > 1) iconButton( Icon(CupertinoIcons.xmark, size: 15, color: colorGrey2), () => _onRemoveObjective(fields), size: 36, ), ], ), const SizedBox(height: 6), Padding( padding: const EdgeInsets.only(right: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ inputField("Name", fields.title, hint: "Distance run", validator: Validators.title), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: inputField( "Target", fields.target, hint: "200", validator: _validateTarget, keyboard: const TextInputType.numberWithOptions( decimal: true), formatters: [ FilteringTextInputFormatter.allow(RegExp(r"[0-9.]")), ], ), ), const SizedBox(width: 10), Expanded( child: inputField("Unit", fields.unit, hint: "km", validator: _validateUnit), ), ], ), ], ), ), ], ), ), ); } // ── ConnectGroupGoalEditor ──────────────────────────────────────────────── @override void onGoalSaved() { Navigator.pop(context, true); } }