Add tick boxes to task rows

Tasks now carry a tick box rather than being read-only rows. Ticking strikes
the title through and leaves the item in place, so a day still reads as a
record of what was asked rather than only what is left.

New in designs/Checkbox.dart:

- TickBox   the square box itself, animating between empty and filled
- TickRow   box + struck-through title + caller-supplied detail and trailing
- addTaskAffordance  the centred "Add new task" control beneath a day's list

Applied to the home plan, the overdue queue and the goal task list. Each
screen holds a set of optimistically ticked ids so the tick lands immediately
and rolls back if the call fails; the set is reconciled whenever fresh data
arrives.

A tick completes the item outright, whatever its proof requirement. The proof
flow is still reachable from the Complete button in the overdue queue, so
tasks that need an artefact have a route that asks for one — but the tick
itself no longer enforces it.

Late completes stay visually distinct: the row keeps a Late pill and the
warning accent, and the user is told the window had already closed.

flutter analyze lib/ reports no errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
This commit is contained in:
alvocool
2026-07-31 21:51:15 +03:00
parent 16bff634b5
commit c88887b639
10 changed files with 496 additions and 166 deletions

View File

@@ -9,6 +9,7 @@ import '../../about/internal/application/NotificationType.dart';
import '../../about/internal/application/ProofType.dart';
import '../../about/internal/application/Standing.dart';
import '../../about/internal/application/TextType.dart';
import '../../designs/Checkbox.dart';
import '../../designs/Component.dart';
import '../../designs/Responsive.dart';
import '../../designs/Shell.dart';
@@ -36,6 +37,9 @@ class OverdueQueueState extends State<OverdueQueue>
bool _changed = false;
/// Ticked locally, awaiting the server. Rolls back on failure.
final Set<String> _ticked = <String>{};
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewOverdueQueue>.reactive(
@@ -66,6 +70,27 @@ class OverdueQueueState extends State<OverdueQueue>
Navigator.pop(context, _changed);
}
/// The tick completes the item outright. The Complete button still runs the
/// proof flow, so a task that needs an artefact has a route that asks for
/// one — the tick is the quick path, not the only one.
void _onTick(Commitment item) {
if (item.id == null || _ticked.contains(item.id)) {
return;
}
setState(() {
_ticked.add(item.id!);
});
_model?.complete(
item,
CompletionRequest(
commitmentId: item.id ?? "",
proofType: item.proofType.name,
),
);
}
void _onComplete(Commitment item) {
// Honour proof settles immediately; everything else has to produce its
// artefact before the completion is accepted.
@@ -526,31 +551,19 @@ class OverdueQueueState extends State<OverdueQueue>
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
text(item.title, 17, TextType.Medium,
color: colorPrimaryDark, maxLines: 2,
overflow: TextOverflow.ellipsis),
const SizedBox(height: 8),
text(formatWindow(item), 11, TextType.Regular,
color: colorGrey2),
],
),
),
const SizedBox(width: 10),
pill(
classLabel(item.commitmentClass),
accent,
classBackground(item.commitmentClass),
textSize: 9,
),
],
TickRow(
checked: _ticked.contains(item.id),
title: item.title,
accent: accent,
onTick: () => _onTick(item),
detail: text(formatWindow(item), 11, TextType.Regular,
color: colorGrey2),
trailing: pill(
classLabel(item.commitmentClass),
accent,
classBackground(item.commitmentClass),
textSize: 9,
),
),
const SizedBox(height: 16),
Row(
@@ -625,6 +638,7 @@ class OverdueQueueState extends State<OverdueQueue>
@override
void onQueueLoaded(List<Commitment> queue, Standing standing, double debt) {
setState(() {
_ticked.removeWhere((id) => queue.every((item) => item.id != id));
_queue = queue;
_standing = standing;
_debt = debt;