Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
Projects: Brick-backed model with list/detail screens and nav entry. Work Logs: per-contributor time ledger, assignee running-clock + helper logged minutes with Hours/Minutes input; pause-after-save and helper-deduct prompts. Comments: threaded comments with file attachments and Quill composer. Detail screen: tabbed layout (Work Log / Comments / Activity), editable title + description via edit dialog, Quill-rendered descriptions with legacy plain-text fallback. Create dialog: fixed-width (480px max), Quill description editor. Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse. RLS: hardened activity-log actor binding, immutable creator_id/ created_at triggers, SECURITY INVOKER visibility helper, append-only work-log and scoped comment policies. Brick migration for project_id FK on programmer_tasks. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../models/programmer_task.model.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/programmer_tasks_provider.dart';
|
||||
|
||||
/// After the ASSIGNEE saves a work log or comment on their own actively-running
|
||||
/// task, ask whether to pause the timer. No-op for anyone else, or when the task
|
||||
/// isn't the caller's currently-running one.
|
||||
Future<void> maybePromptPauseTimer(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ProgrammerTask task,
|
||||
) async {
|
||||
final userId = ref.read(currentUserIdProvider);
|
||||
if (task.assigneeId != userId) return;
|
||||
final controller = ref.read(programmerTasksControllerProvider);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
// Fresh query — the running-task provider can lag right after a start/switch.
|
||||
final running = await controller.findRunningTaskForCurrentUser();
|
||||
if (running?.id != task.id) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
final pause = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
semanticLabel: 'Pause the timer',
|
||||
title: const Text('Pause the timer?'),
|
||||
content: Text('You logged on "${task.title}". Pause its timer now?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Keep running'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Pause'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (pause == true) {
|
||||
await controller.pauseTask(taskId: task.id);
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Timer paused')));
|
||||
}
|
||||
}
|
||||
|
||||
/// After a HELPER logs Time Consumed on a task they don't own, ask whether to
|
||||
/// deduct that time from their own currently-running task. No-op if they have no
|
||||
/// running (unpaused) task. [helpedTaskId] is recorded for audit.
|
||||
Future<void> maybePromptDeductHelperTime(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
int minutes,
|
||||
String helpedTaskId,
|
||||
) async {
|
||||
if (minutes <= 0) return;
|
||||
final controller = ref.read(programmerTasksControllerProvider);
|
||||
final running = await controller.findRunningTaskForCurrentUser();
|
||||
if (running == null) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final deduct = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
semanticLabel: 'Deduct helped time',
|
||||
title: const Text('Deduct this time?'),
|
||||
content: Text(
|
||||
'You logged $minutes min helping. Deduct it from your active task '
|
||||
'"${running.title}" so your time stays accurate?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Skip'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Deduct'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (deduct == true) {
|
||||
await controller.addAdjustment(
|
||||
taskId: running.id,
|
||||
seconds: minutes * 60,
|
||||
reason: 'helped',
|
||||
sourceTaskId: helpedTaskId,
|
||||
);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Deducted $minutes min from "${running.title}"')),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user