import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/attendance_log.dart'; import '../../models/it_service_request.dart'; import '../../models/pass_slip.dart'; import '../../models/task.dart'; import '../../models/ticket.dart'; import '../../theme/m3_motion.dart'; import '../../utils/app_time.dart'; // ───────────────────────────────────────────────────────────────────────────── // Data class // ───────────────────────────────────────────────────────────────────────────── class LogbookDayActivityData { final String userId; final String name; final DateTime date; final String shiftLabel; final DateTime? scheduledStart; final DateTime? scheduledEnd; final bool isSwapped; final List sessions; final List passSlips; final List tasks; final List tickets; final List serviceRequests; const LogbookDayActivityData({ required this.userId, required this.name, required this.date, required this.shiftLabel, this.scheduledStart, this.scheduledEnd, this.isSwapped = false, required this.sessions, required this.passSlips, required this.tasks, required this.tickets, required this.serviceRequests, }); } // ───────────────────────────────────────────────────────────────────────────── // Timeline event model // ───────────────────────────────────────────────────────────────────────────── enum _DayEventType { shiftScheduled, swapNote, checkIn, passSlipOut, passSlipReturn, checkOut, stillOnDuty, taskCreated, taskStarted, taskCompleted, ticketCreated, ticketResponded, ticketPromoted, isrCreated, isrUpdated, isrCompleted, } class _DayEvent { final _DayEventType type; final DateTime time; final String title; final String? subtitle; const _DayEvent({ required this.type, required this.time, required this.title, this.subtitle, }); } // ───────────────────────────────────────────────────────────────────────────── // Event assembly // ───────────────────────────────────────────────────────────────────────────── List<_DayEvent> _buildEvents(LogbookDayActivityData data) { final events = <_DayEvent>[]; final t = AppTime.formatTime; if (data.scheduledStart != null) { events.add(_DayEvent( type: _DayEventType.shiftScheduled, time: data.scheduledStart!, title: 'Shift scheduled — ${data.shiftLabel}', subtitle: data.scheduledEnd != null ? '${t(data.scheduledStart!)} – ${t(data.scheduledEnd!)}' : t(data.scheduledStart!), )); if (data.isSwapped) { events.add(_DayEvent( type: _DayEventType.swapNote, time: data.scheduledStart!, title: 'Shift received via swap', )); } } final sortedSessions = [...data.sessions] ..sort((a, b) => a.checkInAt.compareTo(b.checkInAt)); for (final session in sortedSessions) { events.add(_DayEvent( type: _DayEventType.checkIn, time: session.checkInAt, title: 'Checked in', subtitle: t(session.checkInAt), )); // Pass slips that started during this session window. final sessionEnd = session.checkOutAt ?? AppTime.now(); for (final slip in data.passSlips) { if (slip.slipStart == null) continue; if (slip.slipStart!.isBefore(session.checkInAt) || slip.slipStart!.isAfter(sessionEnd)) { continue; } events.add(_DayEvent( type: _DayEventType.passSlipOut, time: slip.slipStart!, title: 'Pass slip — left', subtitle: slip.reason, )); if (slip.slipEnd != null) { events.add(_DayEvent( type: _DayEventType.passSlipReturn, time: slip.slipEnd!, title: 'Pass slip — returned', subtitle: '${t(slip.slipStart!)} – ${t(slip.slipEnd!)}', )); } else { events.add(_DayEvent( type: _DayEventType.passSlipReturn, time: slip.slipStart!.add(const Duration(seconds: 1)), title: 'Pass slip — still out', )); } } if (session.checkOutAt != null) { events.add(_DayEvent( type: _DayEventType.checkOut, time: session.checkOutAt!, title: 'Checked out', subtitle: t(session.checkOutAt!), )); } else { events.add(_DayEvent( type: _DayEventType.stillOnDuty, time: AppTime.now(), title: 'Currently on duty', )); } } for (final task in data.tasks) { if (task.startedAt != null) { events.add(_DayEvent( type: _DayEventType.taskStarted, time: task.startedAt!, title: 'Task started', subtitle: task.title, )); } else if (task.completedAt != null) { events.add(_DayEvent( type: _DayEventType.taskCompleted, time: task.completedAt!, title: 'Task completed', subtitle: task.title, )); } else { events.add(_DayEvent( type: _DayEventType.taskCreated, time: task.createdAt, title: 'Task created', subtitle: task.title, )); } } for (final ticket in data.tickets) { if (ticket.promotedAt != null) { events.add(_DayEvent( type: _DayEventType.ticketPromoted, time: ticket.promotedAt!, title: 'Ticket promoted to task', subtitle: ticket.subject, )); } else if (ticket.respondedAt != null) { events.add(_DayEvent( type: _DayEventType.ticketResponded, time: ticket.respondedAt!, title: 'Ticket responded', subtitle: ticket.subject, )); } else { events.add(_DayEvent( type: _DayEventType.ticketCreated, time: ticket.createdAt, title: 'Ticket created', subtitle: ticket.subject, )); } } for (final isr in data.serviceRequests) { if (isr.completedAt != null) { events.add(_DayEvent( type: _DayEventType.isrCompleted, time: isr.completedAt!, title: 'IT request completed', subtitle: isr.requestNumber, )); } else { final isCreatedDay = _sameDay(isr.createdAt, data.date); events.add(_DayEvent( type: isCreatedDay ? _DayEventType.isrCreated : _DayEventType.isrUpdated, time: isCreatedDay ? isr.createdAt : isr.updatedAt, title: isCreatedDay ? 'IT request created' : 'IT request updated', subtitle: isr.requestNumber, )); } } events.sort((a, b) => a.time.compareTo(b.time)); return events; } bool _sameDay(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; // ───────────────────────────────────────────────────────────────────────────── // Main sheet widget // ───────────────────────────────────────────────────────────────────────────── class LogbookDayActivitySheet extends ConsumerWidget { const LogbookDayActivitySheet({super.key, required this.data}); final LogbookDayActivityData data; @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final events = _buildEvents(data); final dateStr = _formatDate(data.date); final initials = _initials(data.name); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Drag handle (mobile) ────────────────────────────── Center( child: Container( margin: const EdgeInsets.only(top: 12, bottom: 4), width: 36, height: 4, decoration: BoxDecoration( color: colors.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), ), // ── Header ─────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(20, 8, 16, 0), child: Row( children: [ CircleAvatar( radius: 22, backgroundColor: colors.primaryContainer, child: Text( initials, style: textTheme.titleSmall?.copyWith( color: colors.onPrimaryContainer, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(data.name, style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), Text(dateStr, style: textTheme.bodySmall?.copyWith( color: colors.onSurfaceVariant, )), ], ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // ── Summary chips ───────────────────────────────────── if (data.tasks.isNotEmpty || data.tickets.isNotEmpty || data.serviceRequests.isNotEmpty || data.passSlips.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(20, 10, 20, 0), child: Wrap( spacing: 6, runSpacing: 4, children: [ if (data.tasks.isNotEmpty) _SummaryChip( label: '${data.tasks.length} task${data.tasks.length == 1 ? '' : 's'}', icon: Icons.task_alt_rounded, color: colors.secondaryContainer, onColor: colors.onSecondaryContainer, ), if (data.tickets.isNotEmpty) _SummaryChip( label: '${data.tickets.length} ticket${data.tickets.length == 1 ? '' : 's'}', icon: Icons.confirmation_num_outlined, color: colors.tertiaryContainer, onColor: colors.onTertiaryContainer, ), if (data.serviceRequests.isNotEmpty) _SummaryChip( label: '${data.serviceRequests.length} ISR${data.serviceRequests.length == 1 ? '' : 's'}', icon: Icons.computer_outlined, color: colors.surfaceContainerHighest, onColor: colors.onSurface, ), if (data.passSlips.isNotEmpty) _SummaryChip( label: '${data.passSlips.length} pass slip${data.passSlips.length == 1 ? '' : 's'}', icon: Icons.badge_outlined, color: colors.primaryContainer, onColor: colors.onPrimaryContainer, ), ], ), ), const SizedBox(height: 12), const Divider(height: 1), // ── Timeline ───────────────────────────────────────── Flexible( child: events.isEmpty ? Padding( padding: const EdgeInsets.all(32), child: Center( child: Text( 'No activity recorded for this day.', style: textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), ), ) : ListView.builder( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), itemCount: events.length, itemBuilder: (context, index) { return M3FadeSlideIn( delay: Duration( milliseconds: index.clamp(0, 14) * 45), child: _TimelineTile( event: events[index], isFirst: index == 0, isLast: index == events.length - 1, ), ); }, ), ), ], ); } static String _formatDate(DateTime d) { const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; final wd = weekdays[d.weekday - 1]; final mo = months[d.month - 1]; return '$wd, $mo ${d.day}, ${d.year}'; } static String _initials(String name) { final parts = name.trim().split(RegExp(r'\s+')); if (parts.isEmpty) return '?'; if (parts.length == 1) return parts[0][0].toUpperCase(); return '${parts[0][0]}${parts[parts.length - 1][0]}'.toUpperCase(); } } // ───────────────────────────────────────────────────────────────────────────── // Summary chip // ───────────────────────────────────────────────────────────────────────────── class _SummaryChip extends StatelessWidget { const _SummaryChip({ required this.label, required this.icon, required this.color, required this.onColor, }); final String label; final IconData icon; final Color color; final Color onColor; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: onColor), const SizedBox(width: 5), Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: onColor, fontWeight: FontWeight.w600, ), ), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Timeline tile // ───────────────────────────────────────────────────────────────────────────── class _TimelineTile extends StatelessWidget { const _TimelineTile({ required this.event, required this.isFirst, required this.isLast, }); final _DayEvent event; final bool isFirst; final bool isLast; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final dotColor = _dotColor(colors); return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Dot + connector line ────────────────────────── SizedBox( width: 28, child: Column( children: [ // Top connector (hidden for first item) SizedBox( height: isFirst ? 6 : 0, width: 2, child: isFirst ? null : ColoredBox(color: colors.outlineVariant), ), // Dot Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: dotColor, border: Border.all( color: colors.surface, width: 1.5, ), ), ), // Bottom connector (hidden for last item) if (!isLast) Expanded( child: Center( child: SizedBox( width: 2, child: ColoredBox(color: colors.outlineVariant), ), ), ), ], ), ), const SizedBox(width: 10), // ── Content ─────────────────────────────────────── Expanded( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.title, style: textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, ), ), if (event.subtitle != null) ...[ const SizedBox(height: 2), Text( event.subtitle!, style: textTheme.bodySmall?.copyWith( color: colors.onSurfaceVariant, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ), const SizedBox(width: 8), Text( AppTime.formatTime(event.time), style: textTheme.labelSmall?.copyWith( color: colors.onSurfaceVariant, ), ), ], ), ), ), ], ), ); } Color _dotColor(ColorScheme colors) { switch (event.type) { case _DayEventType.shiftScheduled: case _DayEventType.swapNote: return colors.outline; case _DayEventType.checkIn: case _DayEventType.checkOut: return colors.primary; case _DayEventType.passSlipOut: case _DayEventType.passSlipReturn: return colors.tertiary; case _DayEventType.stillOnDuty: return colors.primaryContainer; case _DayEventType.taskCreated: case _DayEventType.taskStarted: case _DayEventType.taskCompleted: return colors.secondary; case _DayEventType.ticketCreated: case _DayEventType.ticketResponded: case _DayEventType.ticketPromoted: return colors.secondaryContainer; case _DayEventType.isrCreated: case _DayEventType.isrUpdated: case _DayEventType.isrCompleted: return colors.tertiaryContainer; } } }