part of '../custom_workweek_calendar.dart'; class _OutsideHoursStrip extends StatelessWidget { final DateTime weekStart; final List appointments; final double rulerWidth; final void Function(Appointment) onAppointmentTap; final bool Function(Appointment) isCrossedOut; const _OutsideHoursStrip({ super.key, required this.weekStart, required this.appointments, required this.rulerWidth, required this.onAppointmentTap, required this.isCrossedOut, }); @override Widget build(BuildContext context) { final outside = partitionAppointmentsForWeek( appointments, weekStart, ).outside; if (outside.every((day) => day.isEmpty)) return const SizedBox.shrink(); final theme = Theme.of(context); final maxChipsPerDay = outside .map( (day) => day.length > kOutsideChipsMaxVisible ? kOutsideChipsMaxVisible : day.length, ) .fold(0, (m, c) => c > m ? c : m); final stripHeight = kOutsideStripVerticalPadding * 2 + maxChipsPerDay * kOutsideChipHeight + (maxChipsPerDay > 1 ? (maxChipsPerDay - 1) * kOutsideChipSpacing : 0); return Container( color: theme.colorScheme.surfaceContainerLowest, padding: const EdgeInsets.symmetric( vertical: kOutsideStripVerticalPadding, ), child: SizedBox( height: stripHeight - kOutsideStripVerticalPadding * 2, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: rulerWidth), for (var d = 0; d < 5; d++) Expanded( child: _OutsideDayColumn( appointments: outside[d], onAppointmentTap: onAppointmentTap, isCrossedOut: isCrossedOut, ), ), ], ), ), ); } } class _OutsideDayColumn extends StatelessWidget { final List appointments; final void Function(Appointment) onAppointmentTap; final bool Function(Appointment) isCrossedOut; const _OutsideDayColumn({ required this.appointments, required this.onAppointmentTap, required this.isCrossedOut, }); void _showOverflow(BuildContext context, List hidden) { showDetailsBottomSheet( context, children: (sheetCtx) { final tiles = []; for (var i = 0; i < hidden.length; i++) { if (i > 0) tiles.add(const Divider(height: 1)); final apt = hidden[i]; tiles.add( ListTile( leading: Container( width: 12, height: 12, decoration: BoxDecoration( color: apt.color, borderRadius: BorderRadius.circular(3), ), ), title: Text( apt.subject, style: isCrossedOut(apt) ? const TextStyle(decoration: TextDecoration.lineThrough) : null, ), subtitle: Text(_subtitleFor(apt)), onTap: () { Navigator.of(sheetCtx).pop(); onAppointmentTap(apt); }, ), ); } return tiles; }, ); } static String _subtitleFor(Appointment a) { if (isAllDayLike(a)) return 'Ganztägig'; return '${_hm(a.startTime)}–${_hm(a.endTime)}'; } static String _hm(DateTime t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; @override Widget build(BuildContext context) { if (appointments.isEmpty) return const SizedBox.shrink(); final sorted = [...appointments] ..sort((a, b) { final aLike = isAllDayLike(a); final bLike = isAllDayLike(b); if (aLike && !bLike) return -1; if (!aLike && bLike) return 1; return a.startTime.compareTo(b.startTime); }); final visible = sorted.length <= kOutsideChipsMaxVisible ? sorted : sorted.take(kOutsideChipsMaxVisible - 1).toList(); final overflow = sorted.length <= kOutsideChipsMaxVisible ? const [] : sorted.skip(kOutsideChipsMaxVisible - 1).toList(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var i = 0; i < visible.length; i++) ...[ if (i > 0) const SizedBox(height: kOutsideChipSpacing), SizedBox( height: kOutsideChipHeight, child: _OutsideChip( appointment: visible[i], onTap: () => onAppointmentTap(visible[i]), ), ), ], if (overflow.isNotEmpty) ...[ const SizedBox(height: kOutsideChipSpacing), SizedBox( height: kOutsideChipHeight, child: _OutsideOverflowChip( count: overflow.length, onTap: () => _showOverflow(context, overflow), ), ), ], ], ), ); } } class _OutsideChip extends StatelessWidget { final Appointment appointment; final VoidCallback onTap; const _OutsideChip({required this.appointment, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final allDay = isAllDayLike(appointment); final timeLabel = allDay ? null : '${appointment.startTime.hour.toString().padLeft(2, '0')}:${appointment.startTime.minute.toString().padLeft(2, '0')}'; // Past chips fade further, future/ongoing ones get a more saturated tint // so the strip no longer reads as one uniform grey block. final isPast = appointment.endTime.isBefore(DateTime.now()); final backgroundAlpha = isPast ? 38 : 120; final subjectColor = isPast ? theme.colorScheme.onSurfaceVariant : theme.colorScheme.onSurface; final subjectWeight = isPast ? FontWeight.w400 : FontWeight.w600; return Material( color: appointment.color.withAlpha(backgroundAlpha), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(7)), ), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), child: Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Text( appointment.subject, maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false, style: theme.textTheme.labelSmall?.copyWith( color: subjectColor, fontWeight: subjectWeight, ), ), ), if (timeLabel != null) ...[ const SizedBox(width: 4), Flexible( child: Text( timeLabel, maxLines: 1, overflow: TextOverflow.fade, softWrap: false, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontSize: 10, ), ), ), ], ], ), ), ), ); } } class _OutsideOverflowChip extends StatelessWidget { final int count; final VoidCallback onTap; const _OutsideOverflowChip({required this.count, required this.onTap}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Material( color: theme.colorScheme.secondaryContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Center( child: Text( '+$count weitere', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600, ), ), ), ), ); } }