Activity logs
This commit is contained in:
@@ -488,10 +488,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
canAssign: showAssign,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: _buildTatSection(task),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -2259,12 +2256,35 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
);
|
||||
break;
|
||||
case 'started':
|
||||
timeline.add(_activityRow('Task started', actorName, l.createdAt));
|
||||
{
|
||||
var label = 'Task started';
|
||||
if (latestAssignment != null &&
|
||||
l.actorId == latestAssignment.userId &&
|
||||
responseDuration != null) {
|
||||
final assigneeName =
|
||||
profileById[latestAssignment.userId]?.fullName ??
|
||||
latestAssignment.userId;
|
||||
final resp = responseAt ?? AppTime.now();
|
||||
label =
|
||||
'Task started — Response: ${_formatDuration(responseDuration)} ($assigneeName responded at ${AppTime.formatDate(resp)} ${AppTime.formatTime(resp)})';
|
||||
}
|
||||
timeline.add(_activityRow(label, actorName, l.createdAt));
|
||||
}
|
||||
break;
|
||||
case 'completed':
|
||||
timeline.add(
|
||||
_activityRow('Task completed', actorName, l.createdAt),
|
||||
);
|
||||
{
|
||||
var label = 'Task completed';
|
||||
if (task.startedAt != null) {
|
||||
final start = task.startedAt!;
|
||||
final end = task.completedAt ?? l.createdAt;
|
||||
final exec = end.difference(start);
|
||||
if (exec.inMilliseconds > 0) {
|
||||
label =
|
||||
'Task completed — Execution: ${_formatDuration(exec)} (${AppTime.formatDate(start)} ${AppTime.formatTime(start)} → ${AppTime.formatDate(end)} ${AppTime.formatTime(end)})';
|
||||
}
|
||||
}
|
||||
timeline.add(_activityRow(label, actorName, l.createdAt));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
timeline.add(_activityRow(l.actionType, actorName, l.createdAt));
|
||||
@@ -2272,26 +2292,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
}
|
||||
}
|
||||
|
||||
if (responseDuration != null) {
|
||||
final assigneeName =
|
||||
profileById[latestAssignment!.userId]?.fullName ??
|
||||
latestAssignment.userId;
|
||||
timeline.add(const SizedBox(height: 12));
|
||||
timeline.add(
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.timer, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Response Time: ${_formatDuration(responseDuration)} ($assigneeName responded at ${responseAt!.toLocal().toString()})',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Response and execution times are now merged into the related
|
||||
// 'Task started' and 'Task completed' timeline entries above.
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -2301,46 +2303,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTatSection(Task task) {
|
||||
final animateQueue = task.status == 'queued';
|
||||
final animateExecution = task.startedAt != null && task.completedAt == null;
|
||||
|
||||
if (!animateQueue && !animateExecution) {
|
||||
return _buildTatContent(task, AppTime.now());
|
||||
}
|
||||
|
||||
return StreamBuilder<int>(
|
||||
stream: Stream.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(tick) => tick,
|
||||
).asBroadcastStream(),
|
||||
builder: (context, snapshot) {
|
||||
return _buildTatContent(task, AppTime.now());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTatContent(Task task, DateTime now) {
|
||||
final queueDuration = task.status == 'queued'
|
||||
? now.difference(task.createdAt)
|
||||
: _safeDuration(task.startedAt?.difference(task.createdAt));
|
||||
final executionDuration = task.status == 'queued'
|
||||
? null
|
||||
: task.startedAt == null
|
||||
? null
|
||||
: task.completedAt == null
|
||||
? now.difference(task.startedAt!)
|
||||
: task.completedAt!.difference(task.startedAt!);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Queue duration: ${_formatDuration(queueDuration)}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('Task execution time: ${_formatDuration(executionDuration)}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
// TAT helpers removed; timings are shown inline in the activity timeline.
|
||||
|
||||
Widget _activityRow(String title, String actor, DateTime at) {
|
||||
return Padding(
|
||||
@@ -2361,7 +2324,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
Text(title, style: Theme.of(context).textTheme.bodyMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$actor • ${at.toLocal()}',
|
||||
'$actor • ${AppTime.formatDate(at)} ${AppTime.formatTime(at)}',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
@@ -2372,13 +2335,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Duration? _safeDuration(Duration? duration) {
|
||||
if (duration == null) {
|
||||
return null;
|
||||
}
|
||||
return duration.isNegative ? Duration.zero : duration;
|
||||
}
|
||||
|
||||
String _formatDuration(Duration? duration) {
|
||||
if (duration == null) {
|
||||
return 'Pending';
|
||||
|
||||
Reference in New Issue
Block a user