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,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/profile.model.dart';
|
||||
import '../../../utils/programmer_task_ledger.dart';
|
||||
import '../../../widgets/app_section_header.dart';
|
||||
import '../../../widgets/m3_card.dart';
|
||||
|
||||
/// Per-contributor time breakdown for a task: the assignee's running duration
|
||||
/// plus each helper's logged minutes, and the total.
|
||||
class TimeLedgerCard extends StatelessWidget {
|
||||
const TimeLedgerCard({
|
||||
super.key,
|
||||
required this.ledger,
|
||||
required this.profileById,
|
||||
});
|
||||
|
||||
final TaskTimeLedger ledger;
|
||||
final Map<String, Profile> profileById;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ledger.total == Duration.zero) return const SizedBox.shrink();
|
||||
final tt = Theme.of(context).textTheme;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return M3Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppSectionHeader(
|
||||
'Time ledger',
|
||||
trailing: Text(
|
||||
formatDurationHm(ledger.total),
|
||||
style: tt.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
for (final entry in ledger.all)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
entry.isAssignee
|
||||
? Icons.play_circle_outline
|
||||
: Icons.volunteer_activism_outlined,
|
||||
size: 16,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
profileById[entry.userId]?.fullName ?? 'Unknown',
|
||||
style: tt.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (entry.isAssignee)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text('assignee',
|
||||
style: tt.bodySmall
|
||||
?.copyWith(color: cs.onSurfaceVariant)),
|
||||
),
|
||||
Text(
|
||||
formatDurationHm(entry.duration),
|
||||
style: tt.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user