mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-12-07 21:52:35 +00:00
1007 lines
42 KiB
Dart
1007 lines
42 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:supplements/widgets/info_chip.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import '../models/ingredient.dart';
|
||
import '../models/supplement.dart';
|
||
import '../providers/settings_provider.dart';
|
||
import '../providers/simple_sync_provider.dart';
|
||
import '../providers/supplement_provider.dart';
|
||
import '../services/database_sync_service.dart';
|
||
import '../services/rda_service.dart';
|
||
import '../widgets/supplement_card.dart';
|
||
import 'add_supplement_screen.dart';
|
||
import 'archived_supplements_screen.dart';
|
||
|
||
class SupplementsListScreen extends StatelessWidget {
|
||
const SupplementsListScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('My Supplements'),
|
||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||
actions: [
|
||
Consumer<SimpleSyncProvider>(
|
||
builder: (context, syncProvider, child) {
|
||
if (!syncProvider.isConfigured) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return IconButton(
|
||
icon: syncProvider.isSyncing
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: syncProvider.status == SyncStatus.completed &&
|
||
syncProvider.lastSyncTime != null &&
|
||
DateTime.now().difference(syncProvider.lastSyncTime!).inSeconds < 5
|
||
? const Icon(Icons.check, color: Colors.green)
|
||
: const Icon(Icons.sync),
|
||
onPressed: syncProvider.isSyncing ? null : () {
|
||
syncProvider.syncDatabase();
|
||
},
|
||
tooltip: syncProvider.isSyncing ? 'Syncing...' : 'Force Sync',
|
||
);
|
||
},
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.archive),
|
||
onPressed: () {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (context) => const ArchivedSupplementsScreen(),
|
||
),
|
||
);
|
||
},
|
||
tooltip: 'Archived Supplements',
|
||
),
|
||
],
|
||
),
|
||
body: Consumer2<SupplementProvider, SettingsProvider>(
|
||
builder: (context, provider, settingsProvider, child) {
|
||
if (provider.isLoading) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
if (provider.supplements.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.medication_outlined,
|
||
size: 64,
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'No supplements added yet',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Tap the + button to add your first supplement',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return RefreshIndicator(
|
||
onRefresh: () async {
|
||
await provider.loadSupplements();
|
||
await provider.refreshDailyStatus();
|
||
},
|
||
child: _buildGroupedSupplementsList(context, provider.supplements, settingsProvider),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildGroupedSupplementsList(BuildContext context, List<Supplement> supplements, SettingsProvider settingsProvider) {
|
||
final provider = Provider.of<SupplementProvider>(context, listen: false);
|
||
final groupedSupplements = _groupSupplementsByTimeOfDay(supplements, settingsProvider);
|
||
|
||
return ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
// Daily RDA overview header
|
||
FutureBuilder<Map<String, Map<String, dynamic>>>(
|
||
future: (() async {
|
||
if (provider.todayIntakes.isEmpty) return <String, Map<String, dynamic>>{};
|
||
final dailyItems = <Ingredient>[];
|
||
for (final intake in provider.todayIntakes) {
|
||
final supId = intake['supplement_id'] as int;
|
||
final unitsRaw = intake['unitsTaken'];
|
||
final double units = unitsRaw is int ? unitsRaw.toDouble() : (unitsRaw as double? ?? 1.0);
|
||
final matching = provider.supplements.where((s) => s.id == supId);
|
||
if (matching.isEmpty) continue;
|
||
final sup = matching.first;
|
||
for (final ing in sup.ingredients) {
|
||
dailyItems.add(ing.copyWith(amount: ing.amount * units));
|
||
}
|
||
}
|
||
if (dailyItems.isEmpty) return <String, Map<String, dynamic>>{};
|
||
final service = RdaService();
|
||
final agg = await service.aggregateDailyIntake(
|
||
dailyItems,
|
||
dateOfBirth: settingsProvider.dateOfBirth,
|
||
gender: settingsProvider.gender,
|
||
);
|
||
// Convert to plain map for UI without depending on service types
|
||
final result = <String, Map<String, dynamic>>{};
|
||
agg.forEach((key, value) {
|
||
final v = value; // dynamic
|
||
result[key] = {
|
||
'unitLabel': v.unitLabel,
|
||
'rdaValue': v.rdaValue,
|
||
'rdaValueMin': v.rdaValueMin,
|
||
'rdaValueMax': v.rdaValueMax,
|
||
'ulValue': v.ulValue,
|
||
'total': v.totalAmountInRdaUnit,
|
||
'pctRda': v.percentOfRda,
|
||
'pctUl': v.percentOfUl,
|
||
'lifeStage': v.matchedLifeStageLabel,
|
||
'lifeStageDescription': v.matchedLifeStageDescription,
|
||
'rdaType': v.rdaType,
|
||
'note': v.note,
|
||
'nutrientUl': v.nutrientUl != null
|
||
? {
|
||
'value': v.nutrientUl!.value,
|
||
'unit': v.nutrientUl!.unit,
|
||
'duration': v.nutrientUl!.duration,
|
||
'note': v.nutrientUl!.note,
|
||
}
|
||
: null,
|
||
};
|
||
});
|
||
return result;
|
||
})(),
|
||
builder: (context, snapshot) {
|
||
if (snapshot.connectionState != ConnectionState.done) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
final data = snapshot.data;
|
||
if (data == null || data.isEmpty) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 16),
|
||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.25),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(Icons.health_and_safety, size: 18, color: Theme.of(context).colorScheme.primary),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
"Today's intake ",
|
||
style: Theme.of(context).textTheme.titleSmall,
|
||
),
|
||
Text(
|
||
"(Vs Recommended Dietary Allowance)",
|
||
style: Theme.of(context).textTheme.labelSmall,
|
||
),
|
||
const Spacer(),
|
||
IconButton(
|
||
tooltip: 'Sources & disclaimer',
|
||
icon: const Icon(Icons.info_outline, size: 18),
|
||
onPressed: () => _showRdaSourcesSheet(context),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: data.entries.map((e) {
|
||
final v = e.value;
|
||
final double pct = (v['pctRda'] as double?) ?? 0.0;
|
||
final double? pctUl = v['pctUl'] as double?;
|
||
final pretty = e.key.split('_').map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}').join(' ');
|
||
Color bg = Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5);
|
||
if (pctUl != null && pctUl > 100.0) {
|
||
bg = (pctUl <= 110.0) ? Colors.amber.withOpacity(0.25) : Colors.red.withOpacity(0.25);
|
||
} else if (pct >= 100.0) {
|
||
bg = Colors.green.withOpacity(0.25);
|
||
}
|
||
final color = Theme.of(context).colorScheme.onSurfaceVariant;
|
||
return InkWell(
|
||
onTap: () {
|
||
_showRdaDetailsSheet(context, pretty, v);
|
||
},
|
||
borderRadius: BorderRadius.circular(8),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: bg,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: Theme.of(context).colorScheme.outline.withOpacity(0.3)),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
pretty,
|
||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: color),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
'${pct.toStringAsFixed(pct >= 10 ? 0 : 1)}%',
|
||
style: TextStyle(fontSize: 12, color: color),
|
||
),
|
||
if (pctUl != null) ...[
|
||
const SizedBox(width: 6),
|
||
Icon(
|
||
pctUl > 100.0 ? Icons.warning_amber : Icons.shield_outlined,
|
||
size: 14,
|
||
color: pctUl > 100.0
|
||
? (pctUl <= 110.0 ? Colors.amber : Colors.red)
|
||
: color,
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
if (groupedSupplements['morning']!.isNotEmpty) ...[
|
||
_buildSectionHeader('Morning (${settingsProvider.morningRange})', Icons.wb_sunny, Colors.orange, groupedSupplements['morning']!.length),
|
||
...groupedSupplements['morning']!.map((supplement) =>
|
||
SupplementCard(
|
||
supplement: supplement,
|
||
onTake: () => _showTakeDialog(context, supplement),
|
||
onEdit: () => _editSupplement(context, supplement),
|
||
onDelete: () => _deleteSupplement(context, supplement),
|
||
onArchive: () => _archiveSupplement(context, supplement),
|
||
onDuplicate: () => context.read<SupplementProvider>().duplicateSupplement(supplement.id!),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
],
|
||
|
||
if (groupedSupplements['afternoon']!.isNotEmpty) ...[
|
||
_buildSectionHeader('Afternoon (${settingsProvider.afternoonRange})', Icons.light_mode, Colors.blue, groupedSupplements['afternoon']!.length),
|
||
...groupedSupplements['afternoon']!.map((supplement) =>
|
||
SupplementCard(
|
||
supplement: supplement,
|
||
onTake: () => _showTakeDialog(context, supplement),
|
||
onEdit: () => _editSupplement(context, supplement),
|
||
onDelete: () => _deleteSupplement(context, supplement),
|
||
onArchive: () => _archiveSupplement(context, supplement),
|
||
onDuplicate: () => context.read<SupplementProvider>().duplicateSupplement(supplement.id!),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
],
|
||
|
||
if (groupedSupplements['evening']!.isNotEmpty) ...[
|
||
_buildSectionHeader('Evening (${settingsProvider.eveningRange})', Icons.nightlight_round, Colors.indigo, groupedSupplements['evening']!.length),
|
||
...groupedSupplements['evening']!.map((supplement) =>
|
||
SupplementCard(
|
||
supplement: supplement,
|
||
onTake: () => _showTakeDialog(context, supplement),
|
||
onEdit: () => _editSupplement(context, supplement),
|
||
onDelete: () => _deleteSupplement(context, supplement),
|
||
onArchive: () => _archiveSupplement(context, supplement),
|
||
onDuplicate: () => context.read<SupplementProvider>().duplicateSupplement(supplement.id!),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
],
|
||
|
||
if (groupedSupplements['night']!.isNotEmpty) ...[
|
||
_buildSectionHeader('Night (${settingsProvider.nightRange})', Icons.bedtime, Colors.purple, groupedSupplements['night']!.length),
|
||
...groupedSupplements['night']!.map((supplement) =>
|
||
SupplementCard(
|
||
supplement: supplement,
|
||
onTake: () => _showTakeDialog(context, supplement),
|
||
onEdit: () => _editSupplement(context, supplement),
|
||
onDelete: () => _deleteSupplement(context, supplement),
|
||
onArchive: () => _archiveSupplement(context, supplement),
|
||
onDuplicate: () => context.read<SupplementProvider>().duplicateSupplement(supplement.id!),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
],
|
||
|
||
if (groupedSupplements['anytime']!.isNotEmpty) ...[
|
||
_buildSectionHeader('Anytime', Icons.schedule, Colors.grey, groupedSupplements['anytime']!.length),
|
||
...groupedSupplements['anytime']!.map((supplement) =>
|
||
SupplementCard(
|
||
supplement: supplement,
|
||
onTake: () => _showTakeDialog(context, supplement),
|
||
onEdit: () => _editSupplement(context, supplement),
|
||
onDelete: () => _deleteSupplement(context, supplement),
|
||
onArchive: () => _archiveSupplement(context, supplement),
|
||
onDuplicate: () => context.read<SupplementProvider>().duplicateSupplement(supplement.id!),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildSectionHeader(String title, IconData icon, Color color, int count) {
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
decoration: BoxDecoration(
|
||
color: color.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: color.withOpacity(0.3),
|
||
width: 1,
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: color.withOpacity(0.2),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
icon,
|
||
size: 20,
|
||
color: color,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title.contains('(') ? title.split('(')[0].trim() : title,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: color,
|
||
),
|
||
),
|
||
if (title.contains('(')) ...[
|
||
Text(
|
||
'(${title.split('(')[1]}',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: color.withOpacity(0.8),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: color.withOpacity(0.15),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Text(
|
||
count.toString(),
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.bold,
|
||
color: color,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Map<String, List<Supplement>> _groupSupplementsByTimeOfDay(List<Supplement> supplements, SettingsProvider settingsProvider) {
|
||
final Map<String, List<Supplement>> grouped = {
|
||
'morning': <Supplement>[],
|
||
'afternoon': <Supplement>[],
|
||
'evening': <Supplement>[],
|
||
'night': <Supplement>[],
|
||
'anytime': <Supplement>[],
|
||
};
|
||
|
||
for (final supplement in supplements) {
|
||
final category = settingsProvider.determineTimeCategory(supplement.reminderTimes);
|
||
grouped[category]!.add(supplement);
|
||
}
|
||
|
||
return grouped;
|
||
}
|
||
|
||
void _showRdaSourcesSheet(BuildContext context) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
showDragHandle: true,
|
||
builder: (context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Dietary reference sources',
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Source: Health Canada Dietary Reference Intakes. Values are matched by your age and sex. Some ULs (e.g., magnesium) apply to supplemental intake only.',
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
ListTile(
|
||
dense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.open_in_new, size: 18),
|
||
title: const Text(
|
||
'Vitamins reference values',
|
||
style: TextStyle(fontSize: 13),
|
||
),
|
||
subtitle: const Text(
|
||
'canada.ca • reference-values-vitamins',
|
||
style: TextStyle(fontSize: 11),
|
||
),
|
||
onTap: () => _launchUrl('https://www.canada.ca/en/health-canada/services/food-nutrition/healthy-eating/dietary-reference-intakes/tables/reference-values-vitamins.html'),
|
||
),
|
||
ListTile(
|
||
dense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: const Icon(Icons.open_in_new, size: 18),
|
||
title: const Text(
|
||
'Elements (minerals) reference values',
|
||
style: TextStyle(fontSize: 13),
|
||
),
|
||
subtitle: const Text(
|
||
'canada.ca • reference-values-elements',
|
||
style: TextStyle(fontSize: 11),
|
||
),
|
||
onTap: () => _launchUrl('https://www.canada.ca/en/health-canada/services/food-nutrition/healthy-eating/dietary-reference-intakes/tables/reference-values-elements.html'),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Disclaimer: Informational only, some of the details in this app are parsed using AI, and may not be accurate. Always consult a healthcare professional for personalized advice.',
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Future<void> _launchUrl(String url) async {
|
||
final uri = Uri.parse(url);
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
}
|
||
|
||
void _showRdaDetailsSheet(BuildContext context, String nutrientPretty, Map<String, dynamic> data) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
showDragHandle: true,
|
||
isScrollControlled: false,
|
||
builder: (context) {
|
||
final unitLabel = (data['unitLabel'] as String?) ?? '';
|
||
final rdaValue = data['rdaValue'] as double?;
|
||
final ulValue = data['ulValue'] as double?;
|
||
final total = data['total'] as double?;
|
||
final pctRda = data['pctRda'] as double?;
|
||
final pctUl = data['pctUl'] as double?;
|
||
final rdaType = data['rdaType'] as String? ?? '';
|
||
final lifeStage = data['lifeStage'] as String? ?? '';
|
||
final note = data['note'] as String?;
|
||
final lifeStageDesc = data['lifeStageDescription'] as String?;
|
||
final rdaValueMin = data['rdaValueMin'] as double?;
|
||
final rdaValueMax = data['rdaValueMax'] as double?;
|
||
final nutrientUl = (data['nutrientUl'] as Map?)?.cast<String, dynamic>();
|
||
return Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// Header row: title and sources button
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
nutrientPretty,
|
||
style: Theme.of(context).textTheme.titleLarge,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
IconButton(
|
||
tooltip: 'Sources & disclaimer',
|
||
icon: const Icon(Icons.info_outline),
|
||
onPressed: () => _showRdaSourcesSheet(context),
|
||
),
|
||
],
|
||
),
|
||
if (lifeStage.isNotEmpty || (lifeStageDesc != null && lifeStageDesc.isNotEmpty)) ...[
|
||
const SizedBox(height: 4),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 4,
|
||
children: [
|
||
if (lifeStage.isNotEmpty)
|
||
InfoChip(
|
||
icon: Icons.person_outline,
|
||
label: 'Life stage: $lifeStage',
|
||
context: context,
|
||
),
|
||
if (lifeStageDesc != null && lifeStageDesc.isNotEmpty)
|
||
InfoChip(
|
||
icon: Icons.info_outline,
|
||
label: lifeStageDesc!,
|
||
context: context,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
const SizedBox(height: 8),
|
||
// Intake vs RDA chips
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: [
|
||
if (total != null)
|
||
InfoChip(
|
||
icon: Icons.local_drink,
|
||
label: 'Intake: ${total.toStringAsFixed(total % 1 == 0 ? 0 : 1)} $unitLabel',
|
||
context: context,
|
||
),
|
||
if (rdaValueMin != null && rdaValueMax != null)
|
||
InfoChip(
|
||
icon: Icons.rule,
|
||
label: 'RDA: ${rdaValueMin!.toStringAsFixed(rdaValueMin! % 1 == 0 ? 0 : 1)}–${rdaValueMax!.toStringAsFixed(rdaValueMax! % 1 == 0 ? 0 : 1)} $unitLabel',
|
||
context: context,
|
||
)
|
||
else if (rdaValue != null)
|
||
InfoChip(
|
||
icon: Icons.rule,
|
||
label: 'RDA: ${rdaValue!.toStringAsFixed(rdaValue! % 1 == 0 ? 0 : 1)} $unitLabel',
|
||
context: context,
|
||
),
|
||
if (pctRda != null)
|
||
InfoChip(
|
||
icon: Icons.percent,
|
||
label: '%RDA: ${pctRda.toStringAsFixed(pctRda >= 10 ? 0 : 1)}%',
|
||
context: context,
|
||
),
|
||
if (ulValue != null)
|
||
InfoChip(
|
||
icon: Icons.shield_outlined,
|
||
label: 'UL: ${ulValue.toStringAsFixed(ulValue % 1 == 0 ? 0 : 1)} $unitLabel',
|
||
context: context,
|
||
),
|
||
if (pctUl != null)
|
||
InfoChip(
|
||
icon: Icons.warning_amber,
|
||
label: '%UL: ${pctUl.toStringAsFixed(pctUl >= 10 ? 0 : 1)}%',
|
||
context: context,
|
||
),
|
||
],
|
||
),
|
||
if (rdaType.isNotEmpty || (note != null && note!.isNotEmpty) || nutrientUl != null) ...[
|
||
const SizedBox(height: 12),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (rdaType.isNotEmpty)
|
||
Text(
|
||
'Basis: $rdaType',
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
if (nutrientUl != null) ...[
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
'Upper limit guidance',
|
||
style: Theme.of(context).textTheme.titleSmall,
|
||
),
|
||
const SizedBox(height: 4),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 4,
|
||
children: [
|
||
InfoChip(
|
||
icon: Icons.shield_moon_outlined,
|
||
label: 'UL: ${nutrientUl['value']} ${nutrientUl['unit']}',
|
||
context: context,
|
||
),
|
||
if ((nutrientUl['duration'] as String?)?.isNotEmpty ?? false)
|
||
InfoChip(
|
||
icon: Icons.schedule,
|
||
label: 'Duration: ${nutrientUl['duration']}',
|
||
context: context,
|
||
),
|
||
],
|
||
),
|
||
if ((nutrientUl['note'] as String?)?.isNotEmpty ?? false) ...[
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
nutrientUl['note'],
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
if (note != null && note!.isNotEmpty) ...[
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
note!,
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
void _showTakeDialog(BuildContext context, Supplement supplement) {
|
||
final unitsController = TextEditingController(text: supplement.numberOfUnits.toString());
|
||
final notesController = TextEditingController();
|
||
DateTime selectedDateTime = DateTime.now();
|
||
bool useCustomTime = false;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => StatefulBuilder(
|
||
builder: (context, setState) {
|
||
return AlertDialog(
|
||
title: Text('Take ${supplement.name}'),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
controller: unitsController,
|
||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||
decoration: InputDecoration(
|
||
labelText: 'Number of ${supplement.unitType}',
|
||
border: const OutlineInputBorder(),
|
||
suffixText: supplement.unitType,
|
||
),
|
||
onChanged: (value) => setState(() {}),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'Total dosage:',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
),
|
||
Text(
|
||
supplement.ingredientsDisplay,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: Theme.of(context).colorScheme.onSurface,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// Time selection section
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.access_time,
|
||
size: 16,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
'When did you take it?',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: RadioListTile<bool>(
|
||
dense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
title: const Text('Just now', style: TextStyle(fontSize: 12)),
|
||
value: false,
|
||
groupValue: useCustomTime,
|
||
onChanged: (value) => setState(() => useCustomTime = value!),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: RadioListTile<bool>(
|
||
dense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
title: const Text('Custom time', style: TextStyle(fontSize: 12)),
|
||
value: true,
|
||
groupValue: useCustomTime,
|
||
onChanged: (value) => setState(() => useCustomTime = value!),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (useCustomTime) ...[
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surface,
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// Date picker
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.calendar_today,
|
||
size: 14,
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
'Date: ${selectedDateTime.day}/${selectedDateTime.month}/${selectedDateTime.year}',
|
||
style: const TextStyle(fontSize: 12),
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: () async {
|
||
final date = await showDatePicker(
|
||
context: context,
|
||
initialDate: selectedDateTime,
|
||
firstDate: DateTime.now().subtract(const Duration(days: 7)),
|
||
lastDate: DateTime.now(),
|
||
);
|
||
if (date != null) {
|
||
setState(() {
|
||
selectedDateTime = DateTime(
|
||
date.year,
|
||
date.month,
|
||
date.day,
|
||
selectedDateTime.hour,
|
||
selectedDateTime.minute,
|
||
);
|
||
});
|
||
}
|
||
},
|
||
child: const Text('Change', style: TextStyle(fontSize: 10)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 4),
|
||
// Time picker
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.access_time,
|
||
size: 14,
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
'Time: ${selectedDateTime.hour.toString().padLeft(2, '0')}:${selectedDateTime.minute.toString().padLeft(2, '0')}',
|
||
style: const TextStyle(fontSize: 12),
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: () async {
|
||
final time = await showTimePicker(
|
||
context: context,
|
||
initialTime: TimeOfDay.fromDateTime(selectedDateTime),
|
||
);
|
||
if (time != null) {
|
||
setState(() {
|
||
selectedDateTime = DateTime(
|
||
selectedDateTime.year,
|
||
selectedDateTime.month,
|
||
selectedDateTime.day,
|
||
time.hour,
|
||
time.minute,
|
||
);
|
||
});
|
||
}
|
||
},
|
||
child: const Text('Change', style: TextStyle(fontSize: 10)),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
TextField(
|
||
controller: notesController,
|
||
decoration: const InputDecoration(
|
||
labelText: 'Notes (optional)',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
maxLines: 2,
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Cancel'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
|
||
// For now, we'll record 0 as total dosage since we're transitioning to ingredients
|
||
// This will be properly implemented when we add the full ingredient tracking
|
||
final totalDosageTaken = 0.0;
|
||
context.read<SupplementProvider>().recordIntake(
|
||
supplement.id!,
|
||
totalDosageTaken,
|
||
unitsTaken: unitsTaken,
|
||
notes: notesController.text.isNotEmpty ? notesController.text : null,
|
||
takenAt: useCustomTime ? selectedDateTime : null,
|
||
);
|
||
Navigator.of(context).pop();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('${supplement.name} recorded!'),
|
||
backgroundColor: Colors.green,
|
||
),
|
||
);
|
||
},
|
||
child: const Text('Record'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
void _editSupplement(BuildContext context, Supplement supplement) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (context) => AddSupplementScreen(supplement: supplement),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _deleteSupplement(BuildContext context, Supplement supplement) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('Delete Supplement'),
|
||
content: Text('Are you sure you want to delete ${supplement.name}?'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Cancel'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
context.read<SupplementProvider>().deleteSupplement(supplement.id!);
|
||
Navigator.of(context).pop();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('${supplement.name} deleted'),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
},
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
child: const Text('Delete'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _archiveSupplement(BuildContext context, Supplement supplement) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('Archive Supplement'),
|
||
content: Text('Are you sure you want to archive ${supplement.name}? You can unarchive it later from the archived supplements list.'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('Cancel'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
context.read<SupplementProvider>().archiveSupplement(supplement.id!);
|
||
Navigator.of(context).pop();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('${supplement.name} archived'),
|
||
backgroundColor: Colors.orange,
|
||
),
|
||
);
|
||
},
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
|
||
child: const Text('Archive'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|