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>
79 lines
2.6 KiB
Dart
79 lines
2.6 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|