import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import '../../../common/theme/theme.dart'; import '../../components/appbar/appbar.dart'; // Schedule model dengan optimasi class Schedule { final DateTime date; final String title; final String? subtitle; final String? time; final Color color; final IconData? icon; final String? avatarUrl; final bool hasDetails; final String? location; final String? instructions; const Schedule({ required this.date, required this.title, this.subtitle, this.time, required this.color, this.icon, this.avatarUrl, this.hasDetails = false, this.location, this.instructions, }); @override bool operator ==(Object other) => identical(this, other) || other is Schedule && runtimeType == other.runtimeType && date == other.date; @override int get hashCode => date.hashCode; } @RoutePage() class SchedulePage extends StatefulWidget { const SchedulePage({super.key}); @override State createState() => _SchedulePageState(); } class _SchedulePageState extends State with TickerProviderStateMixin { late DateTime _selectedDay; late DateTime _focusedDay; late PageController _pageController; late AnimationController _fadeController; late AnimationController _slideController; late AnimationController _rotationController; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation rotationAnimation; final CalendarFormat _calendarFormat = CalendarFormat.week; // Optimized schedules with const constructor static final List _schedules = [ Schedule( date: DateTime.now(), title: 'Morning Shift', subtitle: '8am - 1pm (5h)', time: '08:00', color: AppColor.primary, icon: Icons.work, hasDetails: true, location: 'Office Building A', instructions: 'Check all equipment before starting', ), Schedule( date: DateTime.now().add(Duration(days: 1)), title: 'Night Shift', subtitle: '6pm - 11pm (5h)', time: '18:00', color: AppColor.warning, icon: Icons.nights_stay, hasDetails: true, location: 'Warehouse B', instructions: 'Close all the lights when you exit', ), Schedule( date: DateTime.now().add(Duration(days: 2)), title: 'Stand up Meeting', subtitle: '30 minutes', time: '09:00', color: AppColor.info, icon: Icons.group, hasDetails: true, location: 'Conference Room', instructions: 'Prepare weekly report', ), Schedule( date: DateTime.now().add(Duration(days: 3)), title: 'Training Session', subtitle: '2 hours', time: '14:00', color: AppColor.success, icon: Icons.school, hasDetails: true, location: 'Training Center', instructions: 'Bring notebook and pen', ), Schedule( date: DateTime.now().add(Duration(days: 4)), title: 'Client Meeting', subtitle: '1 hour', time: '10:00', color: Color(0xFF9C27B0), icon: Icons.business, hasDetails: true, location: 'Client Office', instructions: 'Prepare presentation slides', ), ]; @override void initState() { super.initState(); _selectedDay = DateTime.now(); _focusedDay = DateTime.now(); _pageController = PageController(); // Animation controllers _fadeController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _slideController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _rotationController = AnimationController( duration: const Duration(seconds: 20), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut), ); _slideAnimation = Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic), ); rotationAnimation = Tween( begin: 0.0, end: 2 * 3.14159, // 360 degrees in radians ).animate( CurvedAnimation(parent: _rotationController, curve: Curves.linear), ); // Start animations _fadeController.forward(); _slideController.forward(); _rotationController.repeat(); // Infinite rotation } @override void dispose() { _pageController.dispose(); _fadeController.dispose(); _slideController.dispose(); _rotationController.dispose(); super.dispose(); } List _getEventsForDay(DateTime day) { return _schedules.where((schedule) { return isSameDay(schedule.date, day); }).toList(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, body: AnimatedBuilder( animation: _fadeAnimation, builder: (context, child) { return Opacity( opacity: _fadeAnimation.value, child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ _buildSliverAppBar(), _buildSliverCalendar(), _buildSliverScheduleList(), ], ), ); }, ), ); } Widget _buildSliverAppBar() { return SliverAppBar( expandedHeight: 120.0, floating: false, pinned: true, backgroundColor: AppColor.primary, flexibleSpace: CustomAppBar(title: 'Jadwal'), ); } Widget _buildSliverCalendar() { return SliverToBoxAdapter( child: SlideTransition( position: _slideAnimation, child: Container( margin: const EdgeInsets.all(20), padding: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: TableCalendar( firstDay: DateTime.utc(2023, 1, 1), lastDay: DateTime.utc(2025, 12, 31), focusedDay: _focusedDay, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), calendarFormat: _calendarFormat, eventLoader: _getEventsForDay, availableCalendarFormats: const {CalendarFormat.week: 'Week'}, headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, leftChevronVisible: true, rightChevronVisible: true, headerPadding: const EdgeInsets.symmetric(vertical: 16), titleTextStyle: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: AppColor.textPrimary, ), leftChevronIcon: Icon( Icons.chevron_left, color: AppColor.textSecondary, ), rightChevronIcon: Icon( Icons.chevron_right, color: AppColor.textSecondary, ), ), calendarStyle: CalendarStyle( outsideDaysVisible: false, weekendTextStyle: TextStyle( color: AppColor.textLight, fontWeight: FontWeight.w600, ), defaultTextStyle: TextStyle( color: AppColor.textPrimary, fontWeight: FontWeight.w600, ), selectedTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), selectedDecoration: BoxDecoration( color: AppColor.primary, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), todayDecoration: BoxDecoration( color: AppColor.primary.withOpacity(0.2), shape: BoxShape.circle, ), todayTextStyle: TextStyle( color: AppColor.primary, fontWeight: FontWeight.w700, ), markerDecoration: BoxDecoration( color: AppColor.warning, shape: BoxShape.circle, ), markersMaxCount: 1, markerMargin: const EdgeInsets.only(top: 5), markerSize: 6, ), onDaySelected: (selectedDay, focusedDay) { if (!isSameDay(_selectedDay, selectedDay)) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); _animateScheduleChange(); } }, onPageChanged: (focusedDay) { _focusedDay = focusedDay; }, ), ), ), ); } Widget _buildSliverScheduleList() { final selectedEvents = _getEventsForDay(_selectedDay); return selectedEvents.isEmpty ? SliverFillRemaining( child: SlideTransition( position: _slideAnimation, child: _buildEmptyState(), ), ) : SliverPadding( padding: const EdgeInsets.only( top: 8, left: 20, right: 20, bottom: 20, ), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { return SlideTransition( position: _slideAnimation, child: AnimatedContainer( duration: Duration(milliseconds: 200 + (index * 100)), curve: Curves.easeOutCubic, child: _buildModernScheduleItem( selectedEvents[index], index, ), ), ); }, childCount: selectedEvents.length), ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.event_available, size: 64, color: AppColor.primary.withOpacity(0.6), ), ), const SizedBox(height: 24), Text( 'No events today', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: AppColor.textPrimary, ), ), const SizedBox(height: 8), Text( 'Enjoy your free time!', style: TextStyle(fontSize: 16, color: AppColor.textSecondary), ), ], ), ); } Widget _buildModernScheduleItem(Schedule schedule, int index) { return TweenAnimationBuilder( duration: Duration(milliseconds: 300 + (index * 100)), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOutCubic, builder: (context, value, child) { return Transform.translate( offset: Offset(0, 20 * (1 - value)), child: Opacity( opacity: value, child: Container( margin: const EdgeInsets.only(bottom: 16), child: Material( color: Colors.transparent, child: InkWell( onTap: schedule.hasDetails ? () => _showModernScheduleDetails(schedule) : null, borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: schedule.color.withOpacity(0.2), width: 2, ), boxShadow: [ BoxShadow( color: schedule.color.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Row( children: [ // Time indicator Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: schedule.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( schedule.time ?? 'All day', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: schedule.color, ), ), ), const SizedBox(width: 16), // Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( schedule.title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: AppColor.textPrimary, ), ), if (schedule.subtitle != null) ...[ const SizedBox(height: 4), Text( schedule.subtitle!, style: TextStyle( fontSize: 14, color: AppColor.textSecondary, fontWeight: FontWeight.w500, ), ), ], if (schedule.location != null) ...[ const SizedBox(height: 4), Row( children: [ Icon( Icons.location_on, size: 14, color: AppColor.textLight, ), const SizedBox(width: 4), Text( schedule.location!, style: TextStyle( fontSize: 12, color: AppColor.textLight, ), ), ], ), ], ], ), ), // Icon and arrow if (schedule.icon != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: schedule.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( schedule.icon, color: schedule.color, size: 24, ), ), const SizedBox(width: 12), ], if (schedule.hasDetails) Icon( Icons.arrow_forward_ios, size: 16, color: AppColor.textLight, ), ], ), ), ), ), ), ), ); }, ); } void _animateScheduleChange() { _slideController.reset(); _slideController.forward(); } void _showModernScheduleDetails(Schedule schedule) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => _buildModernScheduleDetailsModal(schedule), ); } Widget _buildModernScheduleDetailsModal(Schedule schedule) { return TweenAnimationBuilder( duration: const Duration(milliseconds: 400), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOutCubic, builder: (context, value, child) { return Transform.translate( offset: Offset(0, 50 * (1 - value)), child: Opacity( opacity: value, child: Container( height: MediaQuery.of(context).size.height * 0.75, decoration: BoxDecoration( color: AppColor.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), child: Column( children: [ // Handle bar Container( margin: const EdgeInsets.only(top: 12), width: 40, height: 4, decoration: BoxDecoration( color: AppColor.border, borderRadius: BorderRadius.circular(2), ), ), // Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header with color indicator Row( children: [ Container( width: 4, height: 40, decoration: BoxDecoration( color: schedule.color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( schedule.title, style: TextStyle( fontSize: 28, fontWeight: FontWeight.w800, color: AppColor.textPrimary, ), ), if (schedule.time != null) Text( schedule.time!, style: TextStyle( fontSize: 16, color: schedule.color, fontWeight: FontWeight.w600, ), ), ], ), ), if (schedule.icon != null) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: schedule.color.withOpacity(0.1), borderRadius: BorderRadius.circular(16), ), child: Icon( schedule.icon, color: schedule.color, size: 32, ), ), ], ), const SizedBox(height: 24), // Duration and location info if (schedule.subtitle != null || schedule.location != null) ...[ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.background, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ if (schedule.subtitle != null) ...[ Row( children: [ Icon( Icons.access_time, color: AppColor.textSecondary, size: 20, ), const SizedBox(width: 12), Text( schedule.subtitle!, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), ], ), if (schedule.location != null) const SizedBox(height: 16), ], if (schedule.location != null) Row( children: [ Icon( Icons.location_on, color: AppColor.textSecondary, size: 20, ), const SizedBox(width: 12), Text( schedule.location!, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), ], ), ], ), ), const SizedBox(height: 24), ], // Map placeholder Container( height: 180, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( colors: [ schedule.color.withOpacity(0.1), schedule.color.withOpacity(0.05), ], ), ), child: Stack( children: [ // Map grid pattern CustomPaint( size: const Size(double.infinity, 180), painter: MapGridPainter( color: schedule.color.withOpacity(0.1), ), ), // Location pin Positioned( right: 40, top: 60, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: schedule.color, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: schedule.color.withOpacity( 0.3, ), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: const Icon( Icons.location_on, color: Colors.white, size: 24, ), ), ), ], ), ), const SizedBox(height: 24), // Instructions if (schedule.instructions != null) ...[ Text( 'Instructions', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: AppColor.textPrimary, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.background, borderRadius: BorderRadius.circular(16), border: Border.all( color: schedule.color.withOpacity(0.2), ), ), child: Text( schedule.instructions!, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: AppColor.textPrimary, height: 1.5, ), ), ), const SizedBox(height: 24), ], // PDF attachment Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColor.background, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColor.border), ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.picture_as_pdf, color: Colors.red, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Instructions.pdf', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), Text( '2.4 MB', style: TextStyle( fontSize: 12, color: AppColor.textSecondary, ), ), ], ), ), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: schedule.color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.download, color: schedule.color, size: 20, ), ), ], ), ), ], ), ), ), ], ), ), ), ); }, ); } } // Custom painter for map grid class MapGridPainter extends CustomPainter { final Color color; MapGridPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1; // Draw vertical lines for (int i = 0; i <= 8; i++) { final x = (size.width / 8) * i; canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); } // Draw horizontal lines for (int i = 0; i <= 6; i++) { final y = (size.height / 6) * i; canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }