582 lines
18 KiB
Dart
582 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../models/routine.dart';
|
|
import '../../repositories/routine_repository.dart';
|
|
import '../../theme/app_theme.dart';
|
|
import '../routines/widgets/routine_card.dart';
|
|
|
|
class DashboardScreen extends ConsumerWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final todayRoutinesAsync = ref.watch(todayRoutinesProvider);
|
|
|
|
return Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
// App Bar with greeting
|
|
SliverAppBar(
|
|
expandedHeight: 120,
|
|
floating: true,
|
|
pinned: true,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
title: Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_getGreeting(),
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.normal,
|
|
),
|
|
),
|
|
const Text(
|
|
'Ready to flow?',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.notifications_outlined),
|
|
onPressed: () {
|
|
// Show notifications
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.person_outline),
|
|
onPressed: () {
|
|
// Show profile
|
|
},
|
|
),
|
|
],
|
|
),
|
|
|
|
// Daily Progress Ring
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildDailyProgressCard(context),
|
|
),
|
|
),
|
|
|
|
// Quick Stats
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: _buildQuickStatsRow(),
|
|
),
|
|
),
|
|
|
|
// Today's Routines Header
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Today's Routines",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
// Navigate to all routines
|
|
},
|
|
child: const Text('See All'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Today's Routines List
|
|
todayRoutinesAsync.when(
|
|
data: (routines) {
|
|
if (routines.isEmpty) {
|
|
return const SliverFillRemaining(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.spa_outlined,
|
|
size: 64,
|
|
color: Colors.grey,
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'No routines for today',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Add your first routine to get started!',
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
final routine = routines[index];
|
|
return RoutineCard(
|
|
routine: routine,
|
|
onTap: () => _showRoutineDetails(context, routine),
|
|
onComplete: () => _completeRoutine(context, ref, routine),
|
|
);
|
|
},
|
|
childCount: routines.length,
|
|
),
|
|
);
|
|
},
|
|
loading: () => const SliverFillRemaining(
|
|
child: Center(child: CircularProgressIndicator()),
|
|
),
|
|
error: (error, stack) => SliverFillRemaining(
|
|
child: Center(
|
|
child: Text('Error: $error'),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Bottom padding
|
|
const SliverPadding(
|
|
padding: EdgeInsets.only(bottom: 100),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => _showAddRoutineDialog(context),
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Add Routine'),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getGreeting() {
|
|
final hour = DateTime.now().hour;
|
|
if (hour < 12) return 'Good morning';
|
|
if (hour < 17) return 'Good afternoon';
|
|
return 'Good evening';
|
|
}
|
|
|
|
Widget _buildDailyProgressCard(BuildContext context) {
|
|
return Card(
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Row(
|
|
children: [
|
|
// Progress Ring
|
|
SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
CircularProgressIndicator(
|
|
value: 0.65, // TODO: Calculate from actual data
|
|
strokeWidth: 10,
|
|
backgroundColor: Colors.grey[200],
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Text(
|
|
'65%',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
'Done',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
// Stats
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Today's Progress",
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'7 of 11 routines completed',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
_buildStatChip(
|
|
icon: Icons.local_fire_department,
|
|
label: '5 day streak',
|
|
color: Colors.orange,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatChip({
|
|
required IconData icon,
|
|
required String label,
|
|
required Color color,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 16, color: color),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickStatsRow() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildQuickStatCard(
|
|
icon: Icons.water_drop,
|
|
label: 'Hydration',
|
|
value: '1.5L',
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildQuickStatCard(
|
|
icon: Icons.medication,
|
|
label: 'Meds Taken',
|
|
value: '2/3',
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildQuickStatCard(
|
|
icon: Icons.star,
|
|
label: 'Points',
|
|
value: '245',
|
|
color: Colors.amber,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickStatCard({
|
|
required IconData icon,
|
|
required String label,
|
|
required String value,
|
|
required Color color,
|
|
}) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: color, size: 28),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRoutineDetails(BuildContext context, Routine routine) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => DraggableScrollableSheet(
|
|
initialChildSize: 0.6,
|
|
maxChildSize: 0.9,
|
|
minChildSize: 0.4,
|
|
expand: false,
|
|
builder: (context, scrollController) {
|
|
return SingleChildScrollView(
|
|
controller: scrollController,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[300],
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
routine.category.icon,
|
|
style: const TextStyle(fontSize: 40),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
routine.name,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
routine.category.displayName,
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
if (routine.description != null) ...[
|
|
Text(
|
|
routine.description!,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
_buildDetailRow(
|
|
Icons.schedule,
|
|
'Scheduled for',
|
|
routine.schedule.time,
|
|
),
|
|
_buildDetailRow(
|
|
Icons.repeat,
|
|
'Frequency',
|
|
_getFrequencyText(routine.schedule),
|
|
),
|
|
_buildDetailRow(
|
|
Icons.star,
|
|
'Points',
|
|
'${routine.points} points',
|
|
),
|
|
const SizedBox(height: 30),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
// Mark as complete
|
|
},
|
|
icon: const Icon(Icons.check_circle),
|
|
label: const Text('Mark as Complete'),
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(IconData icon, String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: Colors.grey),
|
|
const SizedBox(width: 12),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getFrequencyText(Schedule schedule) {
|
|
switch (schedule.type) {
|
|
case ScheduleType.daily:
|
|
return 'Every day';
|
|
case ScheduleType.weekly:
|
|
final days = schedule.daysOfWeek?.map((d) {
|
|
final dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
return dayNames[d];
|
|
}).join(', ');
|
|
return days ?? 'Weekly';
|
|
case ScheduleType.specificDate:
|
|
return 'One-time';
|
|
case ScheduleType.interval:
|
|
return 'Every ${schedule.intervalDays} days';
|
|
}
|
|
}
|
|
|
|
void _completeRoutine(BuildContext context, WidgetRef ref, Routine routine) {
|
|
// TODO: Implement completion logic
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('${routine.name} completed! +${routine.points} points'),
|
|
backgroundColor: Colors.green,
|
|
behavior: SnackBarBehavior.floating,
|
|
action: SnackBarAction(
|
|
label: 'UNDO',
|
|
onPressed: () {
|
|
// Undo completion
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAddRoutineDialog(BuildContext context) {
|
|
// TODO: Navigate to add routine screen
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (context) => const AddRoutineSheet(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AddRoutineSheet extends StatelessWidget {
|
|
const AddRoutineSheet({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Add New Routine',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
...RoutineCategory.values.map((category) => ListTile(
|
|
leading: Text(category.icon, style: const TextStyle(fontSize: 28)),
|
|
title: Text(category.displayName),
|
|
onTap: () {
|
|
// Navigate to routine creation with this category
|
|
Navigator.pop(context);
|
|
},
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|