Files
2026-09-28 14:21:22 +08:00

158 lines
4.9 KiB
Dart

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,
),
),
],
],
),
),
],
),
);
}
}