2025-08-13 15:03:57 +07:00

1032 lines
37 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../../common/theme/theme.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<SchedulePage> createState() => _SchedulePageState();
}
class _SchedulePageState extends State<SchedulePage>
with TickerProviderStateMixin {
late DateTime _selectedDay;
late DateTime _focusedDay;
late PageController _pageController;
late AnimationController _fadeController;
late AnimationController _slideController;
late AnimationController _rotationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> rotationAnimation;
final CalendarFormat _calendarFormat = CalendarFormat.week;
// Optimized schedules with const constructor
static final List<Schedule> _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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
rotationAnimation =
Tween<double>(
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<Schedule> _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,
actions: [
Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColor.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
onPressed: () => _showCalendarOptions(),
icon: Icon(
Icons.calendar_view_week,
color: AppColor.white,
size: 24,
),
),
),
],
flexibleSpace: FlexibleSpaceBar(
titlePadding: const EdgeInsets.only(left: 50, bottom: 16),
title: Text(
'My Schedule',
style: TextStyle(
color: AppColor.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.primary, AppColor.primary.withOpacity(0.8)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
Positioned(
right: -20,
top: -20,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: rotationAnimation.value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.1),
),
),
);
},
),
),
Positioned(
left: -30,
bottom: -30,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: -rotationAnimation.value * 0.5,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.white.withOpacity(0.05),
),
),
);
},
),
),
Positioned(
right: 80,
bottom: 30,
child: AnimatedBuilder(
animation: rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: -rotationAnimation.value * 0.2,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColor.white.withOpacity(0.08),
),
),
);
},
),
),
],
),
),
),
);
}
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<Schedule>(
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<double>(
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 _showCalendarOptions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.border,
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Text(
'Calendar Options',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColor.textPrimary,
),
),
),
],
),
),
);
}
void _showModernScheduleDetails(Schedule schedule) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _buildModernScheduleDetailsModal(schedule),
);
}
Widget _buildModernScheduleDetailsModal(Schedule schedule) {
return TweenAnimationBuilder<double>(
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;
}