Files
supplements/lib/widgets/supplement_card.dart
Menno van Leeuwen 7c63eb473b feat: Update theme colors and add Today Schedule screen
- 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
2025-08-31 20:00:32 +02:00

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,
),
),
],
),
);
}
}