feat(programmer): day sheet detail screen
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../models/programmer_day_sheet_event.model.dart';
|
||||
import '../../../theme/app_status_colors.dart';
|
||||
import '../../../utils/app_time.dart';
|
||||
import '../../../widgets/app_section_header.dart';
|
||||
|
||||
/// Maps an event kind to a human-readable label.
|
||||
String _threadLabel(String kind) => switch (kind) {
|
||||
'submitted' => 'Submitted',
|
||||
'auto_submitted' => 'Auto-submitted at end of day',
|
||||
'disapproved' => 'Disapproved',
|
||||
'justified' => 'Justified',
|
||||
'approved' => 'Approved',
|
||||
'amended' => 'Reopened — new work after approval',
|
||||
_ => kind,
|
||||
};
|
||||
|
||||
IconData _threadIcon(String kind) => switch (kind) {
|
||||
'submitted' => Icons.send_outlined,
|
||||
'auto_submitted' => Icons.schedule_send_outlined,
|
||||
'disapproved' => Icons.thumb_down_alt_outlined,
|
||||
'justified' => Icons.chat_bubble_outline,
|
||||
'approved' => Icons.check_circle_outline,
|
||||
'amended' => Icons.redo_outlined,
|
||||
_ => Icons.circle_outlined,
|
||||
};
|
||||
|
||||
StatusKind _threadKind(String kind) => switch (kind) {
|
||||
'approved' => StatusKind.success,
|
||||
'disapproved' => StatusKind.warning,
|
||||
'justified' => StatusKind.info,
|
||||
_ => StatusKind.neutral,
|
||||
};
|
||||
|
||||
/// Renders day-sheet events as a vertical timeline, oldest first.
|
||||
class DaySheetThread extends StatelessWidget {
|
||||
const DaySheetThread({
|
||||
super.key,
|
||||
required this.events,
|
||||
required this.profileNames,
|
||||
});
|
||||
|
||||
final List<ProgrammerDaySheetEvent> events;
|
||||
|
||||
/// Map of userId → display name used to show actor names.
|
||||
final Map<String, String> profileNames;
|
||||
|
||||
@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 (events.isEmpty)
|
||||
Text(
|
||||
'No activity yet.',
|
||||
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
)
|
||||
else
|
||||
for (final event in events)
|
||||
_ThreadRow(
|
||||
event: event,
|
||||
actorName: event.actorId != null
|
||||
? profileNames[event.actorId] ?? 'Someone'
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreadRow extends StatelessWidget {
|
||||
const _ThreadRow({required this.event, this.actorName});
|
||||
|
||||
final ProgrammerDaySheetEvent event;
|
||||
final String? actorName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tt = Theme.of(context).textTheme;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final status = AppStatusColors.of(context);
|
||||
final kind = _threadKind(event.kind);
|
||||
final swatch = status.byKind(kind);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: swatch.container,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
_threadIcon(event.kind),
|
||||
size: 14,
|
||||
color: swatch.onContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
actorName != null
|
||||
? '$actorName · ${_threadLabel(event.kind)}'
|
||||
: _threadLabel(event.kind),
|
||||
style: tt.bodyMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
AppTime.relative(event.createdAt),
|
||||
style: tt.bodySmall
|
||||
?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (event.body != null && event.body!.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.body!,
|
||||
style: tt.bodySmall
|
||||
?.copyWith(color: cs.onSurfaceVariant),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (event.flaggedTaskIds.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Flagged: ${event.flaggedTaskIds.length} task(s)',
|
||||
style: tt.bodySmall?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user