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:
@@ -10,9 +10,9 @@ import '../../about/internal/application/NavigatorType.dart';
|
||||
import '../../about/internal/application/NotificationType.dart';
|
||||
import '../../about/internal/application/Standing.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../about/internal/application/ToneLevel.dart';
|
||||
import '../../about/internal/application/UserDetails.dart';
|
||||
import '../../configs/Navigator.dart';
|
||||
import '../../designs/Checkbox.dart';
|
||||
import '../../designs/Component.dart';
|
||||
import '../../designs/Responsive.dart';
|
||||
import '../../designs/Shell.dart';
|
||||
@@ -20,7 +20,6 @@ import '../../designs/buttons/Buttons.dart';
|
||||
import '../../designs/text/Text.dart';
|
||||
import '../../utils/Colors.dart';
|
||||
import '../../utils/CommonUtils.dart';
|
||||
import '../../utils/DebtEngine.dart';
|
||||
import '../../utils/StandingEngine.dart';
|
||||
import '../../utils/Thresholds.dart';
|
||||
import '../../utils/ToneEngine.dart';
|
||||
@@ -52,6 +51,11 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
|
||||
bool _distressed = false;
|
||||
|
||||
/// Ids ticked locally but not yet confirmed by the server. The row reads as
|
||||
/// done immediately so the tick feels instant, and rolls back if the call
|
||||
/// fails.
|
||||
final Set<String> _ticked = <String>{};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewHome>.reactive(
|
||||
@@ -101,6 +105,18 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
}
|
||||
}
|
||||
|
||||
void _onTick(Commitment item) {
|
||||
if (item.id == null || _ticked.contains(item.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_ticked.add(item.id!);
|
||||
});
|
||||
|
||||
_model?.tick(item);
|
||||
}
|
||||
|
||||
void _onOpenReportCard() {
|
||||
GroundedNavigation().navigateToPage(
|
||||
NavigatorType.justOpen, const ReportCardScreen(), context);
|
||||
@@ -272,9 +288,14 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
"Nothing committed today",
|
||||
"An empty plan is a decision too. Add something you actually intend to do.",
|
||||
)
|
||||
else
|
||||
else ...[
|
||||
..._plan.map(_commitmentRow),
|
||||
const SizedBox(height: 28),
|
||||
addTaskAffordance(
|
||||
_onAddCommitment,
|
||||
enabled: StandingEngine.permitsNewCommitment(_standing),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
_quickLinks(),
|
||||
const SizedBox(height: 24),
|
||||
roundedCornerButton(
|
||||
@@ -499,83 +520,68 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
);
|
||||
}
|
||||
|
||||
/// A task line with its tick box. Completed items keep their place, struck
|
||||
/// through — the day stays a record of what was asked, not just what is left.
|
||||
Widget _commitmentRow(Commitment item) {
|
||||
final bool late = item.windowClosed &&
|
||||
item.status != CommitmentStatus.Completed &&
|
||||
item.status != CommitmentStatus.LateCompleted;
|
||||
final bool settled = item.status == CommitmentStatus.Completed ||
|
||||
item.status == CommitmentStatus.LateCompleted;
|
||||
|
||||
final bool ticked = settled || _ticked.contains(item.id);
|
||||
|
||||
final bool late = item.windowClosed && !settled;
|
||||
|
||||
final Color accent = classColor(item.commitmentClass);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: card(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||
onTap: _onOpenOverdue,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 42,
|
||||
margin: const EdgeInsets.only(right: 14, top: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 14, 8),
|
||||
child: TickRow(
|
||||
checked: ticked,
|
||||
title: item.title,
|
||||
accent: item.status == CommitmentStatus.LateCompleted
|
||||
? colorStandingWarned
|
||||
: accent,
|
||||
onTick: settled ? null : () => _onTick(item),
|
||||
onTap: _onOpenOverdue,
|
||||
detail: Row(
|
||||
children: [
|
||||
text(formatWindow(item), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration:
|
||||
BoxDecoration(color: colorGrey, shape: BoxShape.circle),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(item.title, 15, TextType.Medium,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
text(formatWindow(item), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 10),
|
||||
Container(width: 3, height: 3, decoration: BoxDecoration(
|
||||
color: colorGrey, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 10),
|
||||
text(formatMinutes(item.estMinutes), 11,
|
||||
TextType.Regular, color: colorGrey2),
|
||||
],
|
||||
),
|
||||
if (late) ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
pill(
|
||||
overdueLabel(item),
|
||||
colorStandingGrounded,
|
||||
colorStandingGroundedBg,
|
||||
textSize: 9,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
pill(
|
||||
"−${formatDebt(DebtEngine.commitmentDebt(item))}",
|
||||
colorGrey2,
|
||||
colorMuted,
|
||||
textSize: 9,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
pill(
|
||||
classLabel(item.commitmentClass),
|
||||
accent,
|
||||
classBackground(item.commitmentClass),
|
||||
textSize: 9,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
text(formatMinutes(item.estMinutes), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
if (late) ...[
|
||||
const SizedBox(width: 10),
|
||||
pill(
|
||||
overdueLabel(item),
|
||||
colorStandingGrounded,
|
||||
colorStandingGroundedBg,
|
||||
textSize: 9,
|
||||
),
|
||||
],
|
||||
if (item.status == CommitmentStatus.LateCompleted) ...[
|
||||
const SizedBox(width: 10),
|
||||
pill("Late", colorStandingWarned, colorStandingWarnedBg,
|
||||
textSize: 9),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: ticked
|
||||
? null
|
||||
: pill(
|
||||
classLabel(item.commitmentClass),
|
||||
accent,
|
||||
classBackground(item.commitmentClass),
|
||||
textSize: 9,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -650,6 +656,9 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
void onPlanLoaded(List<Commitment> plan) {
|
||||
setState(() {
|
||||
_plan = plan;
|
||||
// The server is now authoritative; anything it reports as settled no
|
||||
// longer needs the local optimistic flag.
|
||||
_ticked.removeWhere((id) => plan.every((item) => item.id != id));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -682,6 +691,31 @@ class HomeState extends State<Home> implements ConnectHome {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onTicked(Commitment item, bool late) {
|
||||
if (!late) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A clean tick needs no comment. A late one does — otherwise the user
|
||||
// learns that the window never mattered.
|
||||
_model?.showApplicationNotification(
|
||||
NotificationType.warning,
|
||||
"Late complete",
|
||||
"The window had already closed, so this is recorded as a late complete. It reduces the debt but does not clear it.",
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTickFailed(Commitment item) {
|
||||
setState(() {
|
||||
_ticked.remove(item.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onCreationBlocked(String reason) {
|
||||
_model?.showApplicationNotification(
|
||||
|
||||
Reference in New Issue
Block a user