b307f66f2c
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>
104 lines
3.2 KiB
Dart
104 lines
3.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../models/profile.model.dart';
|
|
import '../../../models/programmer_task_activity_log.model.dart';
|
|
import '../../../utils/app_time.dart';
|
|
import '../../../widgets/app_section_header.dart';
|
|
|
|
/// The task's activity timeline (created/started/paused/adjusted/…), newest first.
|
|
class ActivityTimeline extends StatelessWidget {
|
|
const ActivityTimeline({
|
|
super.key,
|
|
required this.logs,
|
|
required this.profileById,
|
|
});
|
|
|
|
final List<ProgrammerTaskActivityLog> logs;
|
|
final Map<String, Profile> profileById;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tt = Theme.of(context).textTheme;
|
|
final cs = Theme.of(context).colorScheme;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const AppSectionHeader('Activity'),
|
|
if (logs.isEmpty)
|
|
Text('No activity yet.',
|
|
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant))
|
|
else
|
|
for (final log in logs)
|
|
_ActivityRow(log: log, profileById: profileById),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ActivityRow extends StatelessWidget {
|
|
const _ActivityRow({required this.log, required this.profileById});
|
|
|
|
final ProgrammerTaskActivityLog log;
|
|
final Map<String, Profile> profileById;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tt = Theme.of(context).textTheme;
|
|
final cs = Theme.of(context).colorScheme;
|
|
final actor = log.actorId == null ? null : profileById[log.actorId];
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(_iconFor(log.actionType), size: 16, color: cs.onSurfaceVariant),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'${actor?.fullName ?? 'Someone'} · ${_labelFor(log.actionType)}',
|
|
style: tt.bodyMedium,
|
|
),
|
|
Text(
|
|
AppTime.relative(log.createdAt),
|
|
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _labelFor(String action) => switch (action) {
|
|
'created' => 'created the task',
|
|
'started' => 'started work',
|
|
'paused' => 'paused',
|
|
'resumed' => 'resumed',
|
|
'completed' => 'completed the task',
|
|
'cancelled' => 'cancelled the task',
|
|
'assigned' => 'assigned the task',
|
|
'reassigned' => 'reassigned the task',
|
|
'adjusted' || 'adjustment' => 'adjusted time',
|
|
'updated' => 'updated details',
|
|
_ => action,
|
|
};
|
|
|
|
IconData _iconFor(String action) => switch (action) {
|
|
'created' => Icons.add_task,
|
|
'started' => Icons.play_arrow_rounded,
|
|
'paused' => Icons.pause_rounded,
|
|
'resumed' => Icons.play_arrow_rounded,
|
|
'completed' => Icons.check_circle_outline,
|
|
'cancelled' => Icons.cancel_outlined,
|
|
'assigned' || 'reassigned' => Icons.person_add_alt_1_outlined,
|
|
'adjustment' => Icons.timelapse_outlined,
|
|
'updated' => Icons.edit_outlined,
|
|
_ => Icons.circle_outlined,
|
|
};
|