mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
- Changed color scheme from ShadBlueColorScheme to ShadZincColorScheme in main.dart - Added getMostRecentIntake method in SupplementProvider to retrieve the latest intake for a supplement - Integrated TodayScheduleScreen into HomeScreen with a new BottomNavigationBar item - Updated SupplementsListScreen title and adjusted layout for better UX - Enhanced SupplementCard to support undoing last taken action and improved popover menu options - Added popover package for better UI interactions
366 lines
15 KiB
Dart
366 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
|
import 'package:popover/popover.dart';
|
|
import '../models/supplement.dart';
|
|
import '../providers/supplement_provider.dart';
|
|
|
|
class SupplementCard extends StatefulWidget {
|
|
final Supplement supplement;
|
|
final VoidCallback onTake;
|
|
final VoidCallback onEdit;
|
|
final VoidCallback onDelete;
|
|
final VoidCallback onArchive;
|
|
final VoidCallback onDuplicate;
|
|
final VoidCallback? onUndoLastTaken;
|
|
final bool showCompletionStatus;
|
|
|
|
const SupplementCard({
|
|
super.key,
|
|
required this.supplement,
|
|
required this.onTake,
|
|
required this.onEdit,
|
|
required this.onDelete,
|
|
required this.onArchive,
|
|
required this.onDuplicate,
|
|
this.onUndoLastTaken,
|
|
this.showCompletionStatus = true,
|
|
});
|
|
|
|
@override
|
|
State<SupplementCard> createState() => _SupplementCardState();
|
|
}
|
|
|
|
class _SupplementCardState extends State<SupplementCard> {
|
|
bool _isExpanded = false;
|
|
|
|
Widget _buildPopoverItem({
|
|
required IconData icon,
|
|
required String label,
|
|
required VoidCallback onTap,
|
|
Color? color,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 18,
|
|
color: color ?? Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: color ?? Theme.of(context).colorScheme.onSurface,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<SupplementProvider>(
|
|
builder: (context, provider, child) {
|
|
final bool isTakenToday = widget.showCompletionStatus ? provider.hasBeenTakenToday(widget.supplement.id!) : false;
|
|
final int todayIntakeCount = widget.showCompletionStatus ? provider.getTodayIntakeCount(widget.supplement.id!) : 0;
|
|
final bool isCompletelyTaken = widget.showCompletionStatus ? todayIntakeCount >= widget.supplement.frequencyPerDay : false;
|
|
|
|
// Get today's intake times for this supplement (only if showing completion status)
|
|
final todayIntakes = widget.showCompletionStatus ? provider.todayIntakes
|
|
.where((intake) => intake['supplement_id'] == widget.supplement.id)
|
|
.map((intake) {
|
|
final takenAt = DateTime.parse(intake['takenAt']);
|
|
final unitsTaken = intake['unitsTaken'] ?? 1.0;
|
|
return {
|
|
'time': '${takenAt.hour.toString().padLeft(2, '0')}:${takenAt.minute.toString().padLeft(2, '0')}',
|
|
'units': unitsTaken is int ? unitsTaken.toDouble() : unitsTaken as double,
|
|
};
|
|
}).toList() : [];
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: InkWell(
|
|
onTap: () {
|
|
showPopover(
|
|
context: context,
|
|
bodyBuilder: (context) => Container(
|
|
constraints: const BoxConstraints(maxWidth: 200),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
|
|
width: 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Theme.of(context).colorScheme.shadow.withValues(alpha: 0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (widget.onUndoLastTaken != null && isTakenToday)
|
|
_buildPopoverItem(
|
|
icon: Icons.undo,
|
|
label: 'Undo Last Taken',
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
widget.onUndoLastTaken!();
|
|
},
|
|
color: Colors.orange,
|
|
),
|
|
_buildPopoverItem(
|
|
icon: Icons.edit,
|
|
label: 'Edit',
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
widget.onEdit();
|
|
},
|
|
),
|
|
_buildPopoverItem(
|
|
icon: Icons.copy,
|
|
label: 'Duplicate',
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
widget.onDuplicate();
|
|
},
|
|
),
|
|
_buildPopoverItem(
|
|
icon: Icons.archive,
|
|
label: 'Archive',
|
|
color: Colors.orange,
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
widget.onArchive();
|
|
},
|
|
),
|
|
_buildPopoverItem(
|
|
icon: Icons.delete,
|
|
label: 'Delete',
|
|
color: Colors.red,
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
widget.onDelete();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
direction: PopoverDirection.bottom,
|
|
width: 180,
|
|
height: null,
|
|
arrowHeight: 0,
|
|
arrowWidth: 0,
|
|
backgroundColor: Colors.transparent,
|
|
shadow: const [],
|
|
);
|
|
},
|
|
borderRadius: BorderRadius.circular(12),
|
|
splashColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4),
|
|
highlightColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Theme.of(context).colorScheme.surface
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.secondaryContainer
|
|
: Theme.of(context).colorScheme.surface) : Theme.of(context).colorScheme.surface,
|
|
border: Border.all(
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Colors.green.shade600
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.secondary
|
|
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2)) : Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Colors.green.shade500
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.secondary
|
|
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1)) : Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Icons.check_circle
|
|
: isTakenToday
|
|
? Icons.schedule
|
|
: Icons.medication) : Icons.medication,
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Theme.of(context).colorScheme.inverseSurface
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.onSecondary
|
|
: Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary,
|
|
size: 18,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.supplement.name,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Theme.of(context).colorScheme.inverseSurface
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.onSecondaryContainer
|
|
: Theme.of(context).colorScheme.onSurface) : Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
if (widget.showCompletionStatus && isCompletelyTaken) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade500,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
'Complete',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.inverseSurface,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
if (widget.supplement.brand != null && widget.supplement.brand!.isNotEmpty)
|
|
Text(
|
|
widget.supplement.brand!,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: widget.showCompletionStatus ? (isCompletelyTaken
|
|
? Colors.green.shade200
|
|
: isTakenToday
|
|
? Theme.of(context).colorScheme.secondary
|
|
: Theme.of(context).colorScheme.primary) : Theme.of(context).colorScheme.primary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'${widget.supplement.frequencyPerDay}x daily • ${widget.supplement.numberOfUnits} ${widget.supplement.unitType}',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: ShadTheme.of(context).colorScheme.foreground.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
if (widget.showCompletionStatus && isTakenToday && !isCompletelyTaken) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'$todayIntakeCount/${widget.supplement.frequencyPerDay}',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSecondary,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InfoChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final BuildContext context;
|
|
final bool fullWidth;
|
|
|
|
const _InfoChip({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.context,
|
|
this.fullWidth = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: fullWidth ? double.infinity : null,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.4),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 14,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Flexible(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|