initial commit

Signed-off-by: Menno van Leeuwen <menno@vleeuwen.me>
This commit is contained in:
2025-08-26 01:21:26 +02:00
commit f8c19f9051
132 changed files with 7054 additions and 0 deletions

39
lib/main.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/supplement_provider.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => SupplementProvider()..initialize(),
child: MaterialApp(
title: 'Supplements Tracker',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system, // Follows system theme
home: const HomeScreen(),
debugShowCheckedModeBanner: false,
),
);
}
}

View File

@@ -0,0 +1,90 @@
class Supplement {
final int? id;
final String name;
final double dosageAmount; // Amount per unit (e.g., 187mg)
final int numberOfUnits; // Number of units to take (e.g., 2 capsules)
final String unit; // mg, g, ml, etc.
final String unitType; // capsules, tablets, ml, etc.
final int frequencyPerDay;
final List<String> reminderTimes; // e.g., ['08:00', '20:00']
final String? notes;
final DateTime createdAt;
final bool isActive;
Supplement({
this.id,
required this.name,
required this.dosageAmount,
required this.numberOfUnits,
required this.unit,
required this.unitType,
required this.frequencyPerDay,
required this.reminderTimes,
this.notes,
required this.createdAt,
this.isActive = true,
});
// Helper getter for total dosage per intake
double get totalDosagePerIntake => dosageAmount * numberOfUnits;
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'dosageAmount': dosageAmount,
'numberOfUnits': numberOfUnits,
'unit': unit,
'unitType': unitType,
'frequencyPerDay': frequencyPerDay,
'reminderTimes': reminderTimes.join(','),
'notes': notes,
'createdAt': createdAt.toIso8601String(),
'isActive': isActive ? 1 : 0,
};
}
factory Supplement.fromMap(Map<String, dynamic> map) {
return Supplement(
id: map['id'],
name: map['name'],
dosageAmount: map['dosageAmount']?.toDouble() ?? map['dosage']?.toDouble() ?? 0.0, // Backwards compatibility
numberOfUnits: map['numberOfUnits'] ?? 1, // Default to 1 for backwards compatibility
unit: map['unit'],
unitType: map['unitType'] ?? 'units', // Default unit type for backwards compatibility
frequencyPerDay: map['frequencyPerDay'],
reminderTimes: map['reminderTimes'].split(','),
notes: map['notes'],
createdAt: DateTime.parse(map['createdAt']),
isActive: map['isActive'] == 1,
);
}
Supplement copyWith({
int? id,
String? name,
double? dosageAmount,
int? numberOfUnits,
String? unit,
String? unitType,
int? frequencyPerDay,
List<String>? reminderTimes,
String? notes,
DateTime? createdAt,
bool? isActive,
}) {
return Supplement(
id: id ?? this.id,
name: name ?? this.name,
dosageAmount: dosageAmount ?? this.dosageAmount,
numberOfUnits: numberOfUnits ?? this.numberOfUnits,
unit: unit ?? this.unit,
unitType: unitType ?? this.unitType,
frequencyPerDay: frequencyPerDay ?? this.frequencyPerDay,
reminderTimes: reminderTimes ?? this.reminderTimes,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
isActive: isActive ?? this.isActive,
);
}
}

View File

@@ -0,0 +1,57 @@
class SupplementIntake {
final int? id;
final int supplementId;
final DateTime takenAt;
final double dosageTaken; // Total dosage amount taken
final int unitsTaken; // Number of units taken
final String? notes;
SupplementIntake({
this.id,
required this.supplementId,
required this.takenAt,
required this.dosageTaken,
required this.unitsTaken,
this.notes,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'supplementId': supplementId,
'takenAt': takenAt.toIso8601String(),
'dosageTaken': dosageTaken,
'unitsTaken': unitsTaken,
'notes': notes,
};
}
factory SupplementIntake.fromMap(Map<String, dynamic> map) {
return SupplementIntake(
id: map['id'],
supplementId: map['supplementId'],
takenAt: DateTime.parse(map['takenAt']),
dosageTaken: map['dosageTaken'],
unitsTaken: map['unitsTaken'] ?? 1, // Default for backwards compatibility
notes: map['notes'],
);
}
SupplementIntake copyWith({
int? id,
int? supplementId,
DateTime? takenAt,
double? dosageTaken,
int? unitsTaken,
String? notes,
}) {
return SupplementIntake(
id: id ?? this.id,
supplementId: supplementId ?? this.supplementId,
takenAt: takenAt ?? this.takenAt,
dosageTaken: dosageTaken ?? this.dosageTaken,
unitsTaken: unitsTaken ?? this.unitsTaken,
notes: notes ?? this.notes,
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/foundation.dart';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
import '../services/database_helper.dart';
import '../services/notification_service.dart';
class SupplementProvider with ChangeNotifier {
final DatabaseHelper _databaseHelper = DatabaseHelper.instance;
final NotificationService _notificationService = NotificationService();
List<Supplement> _supplements = [];
List<Map<String, dynamic>> _todayIntakes = [];
List<Map<String, dynamic>> _monthlyIntakes = [];
bool _isLoading = false;
List<Supplement> get supplements => _supplements;
List<Map<String, dynamic>> get todayIntakes => _todayIntakes;
List<Map<String, dynamic>> get monthlyIntakes => _monthlyIntakes;
bool get isLoading => _isLoading;
Future<void> initialize() async {
await _notificationService.initialize();
await _notificationService.requestPermissions();
await loadSupplements();
await loadTodayIntakes();
}
Future<void> loadSupplements() async {
_isLoading = true;
notifyListeners();
try {
print('Loading supplements from database...');
_supplements = await _databaseHelper.getAllSupplements();
print('Loaded ${_supplements.length} supplements');
for (var supplement in _supplements) {
print('Supplement: ${supplement.name}');
}
} catch (e) {
print('Error loading supplements: $e');
if (kDebugMode) {
print('Error loading supplements: $e');
}
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> addSupplement(Supplement supplement) async {
try {
print('Adding supplement: ${supplement.name}');
final id = await _databaseHelper.insertSupplement(supplement);
print('Supplement inserted with ID: $id');
final newSupplement = supplement.copyWith(id: id);
// Schedule notifications (skip if there's an error)
try {
await _notificationService.scheduleSupplementReminders(newSupplement);
print('Notifications scheduled');
} catch (notificationError) {
print('Warning: Could not schedule notifications: $notificationError');
}
await loadSupplements();
print('Supplements reloaded, count: ${_supplements.length}');
} catch (e) {
print('Error adding supplement: $e');
if (kDebugMode) {
print('Error adding supplement: $e');
}
rethrow;
}
}
Future<void> updateSupplement(Supplement supplement) async {
try {
await _databaseHelper.updateSupplement(supplement);
// Reschedule notifications
await _notificationService.scheduleSupplementReminders(supplement);
await loadSupplements();
} catch (e) {
if (kDebugMode) {
print('Error updating supplement: $e');
}
}
}
Future<void> deleteSupplement(int id) async {
try {
await _databaseHelper.deleteSupplement(id);
// Cancel notifications
await _notificationService.cancelSupplementReminders(id);
await loadSupplements();
} catch (e) {
if (kDebugMode) {
print('Error deleting supplement: $e');
}
}
}
Future<void> recordIntake(int supplementId, double dosage, {int? unitsTaken, String? notes}) async {
try {
final intake = SupplementIntake(
supplementId: supplementId,
takenAt: DateTime.now(),
dosageTaken: dosage,
unitsTaken: unitsTaken ?? 1,
notes: notes,
);
await _databaseHelper.insertIntake(intake);
await loadTodayIntakes();
// Show confirmation notification
final supplement = _supplements.firstWhere((s) => s.id == supplementId);
final unitsText = unitsTaken != null && unitsTaken > 1 ? '$unitsTaken ${supplement.unitType}' : '';
await _notificationService.showInstantNotification(
'Supplement Taken',
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})',
);
} catch (e) {
if (kDebugMode) {
print('Error recording intake: $e');
}
}
}
Future<void> loadTodayIntakes() async {
try {
_todayIntakes = await _databaseHelper.getIntakesWithSupplementsForDate(DateTime.now());
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('Error loading today\'s intakes: $e');
}
}
}
Future<void> loadMonthlyIntakes(int year, int month) async {
try {
_monthlyIntakes = await _databaseHelper.getIntakesWithSupplementsForMonth(year, month);
notifyListeners();
} catch (e) {
if (kDebugMode) {
print('Error loading monthly intakes: $e');
}
}
}
Future<List<Map<String, dynamic>>> getIntakesForDate(DateTime date) async {
try {
return await _databaseHelper.getIntakesWithSupplementsForDate(date);
} catch (e) {
if (kDebugMode) {
print('Error loading intakes for date: $e');
}
return [];
}
}
Future<void> deleteIntake(int intakeId) async {
try {
await _databaseHelper.deleteIntake(intakeId);
await loadTodayIntakes();
} catch (e) {
if (kDebugMode) {
print('Error deleting intake: $e');
}
}
}
}

View File

@@ -0,0 +1,388 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
import '../models/supplement.dart';
import '../providers/supplement_provider.dart';
class AddSupplementScreen extends StatefulWidget {
final Supplement? supplement;
const AddSupplementScreen({super.key, this.supplement});
@override
State<AddSupplementScreen> createState() => _AddSupplementScreenState();
}
class _AddSupplementScreenState extends State<AddSupplementScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _dosageAmountController = TextEditingController();
final _numberOfUnitsController = TextEditingController();
final _notesController = TextEditingController();
String _selectedUnit = 'mg';
String _selectedUnitType = 'capsules';
int _frequencyPerDay = 1;
List<String> _reminderTimes = ['08:00'];
final List<String> _units = ['mg', 'g', 'μg', 'IU', 'ml'];
final List<String> _unitTypes = ['capsules', 'tablets', 'softgels', 'drops', 'ml', 'scoops', 'gummies'];
@override
void initState() {
super.initState();
if (widget.supplement != null) {
_initializeWithExistingSupplement();
} else {
_numberOfUnitsController.text = '1'; // Default to 1 unit
}
}
void _initializeWithExistingSupplement() {
final supplement = widget.supplement!;
_nameController.text = supplement.name;
_dosageAmountController.text = supplement.dosageAmount.toString();
_numberOfUnitsController.text = supplement.numberOfUnits.toString();
_notesController.text = supplement.notes ?? '';
_selectedUnit = supplement.unit;
_selectedUnitType = supplement.unitType;
_frequencyPerDay = supplement.frequencyPerDay;
_reminderTimes = List.from(supplement.reminderTimes);
}
@override
Widget build(BuildContext context) {
final isEditing = widget.supplement != null;
return Scaffold(
appBar: AppBar(
title: Text(isEditing ? 'Edit Supplement' : 'Add Supplement'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name field
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Supplement Name *',
border: OutlineInputBorder(),
hintText: 'e.g., Vitamin D3',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a supplement name';
}
return null;
},
),
const SizedBox(height: 16),
// Dosage amount per unit
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: _dosageAmountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Amount per unit *',
border: OutlineInputBorder(),
hintText: '187',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter amount per unit';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: DropdownButtonFormField<String>(
value: _selectedUnit,
decoration: const InputDecoration(
labelText: 'Unit',
border: OutlineInputBorder(),
),
items: _units.map((unit) {
return DropdownMenuItem(
value: unit,
child: Text(unit),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedUnit = value!;
});
},
),
),
],
),
const SizedBox(height: 16),
// Number of units to take
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: _numberOfUnitsController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Number to take *',
border: OutlineInputBorder(),
hintText: '2',
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter number to take';
}
if (int.tryParse(value) == null || int.parse(value) < 1) {
return 'Please enter a valid number (1 or more)';
}
return null;
},
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: DropdownButtonFormField<String>(
value: _selectedUnitType,
decoration: const InputDecoration(
labelText: 'Type',
border: OutlineInputBorder(),
),
items: _unitTypes.map((type) {
return DropdownMenuItem(
value: type,
child: Text(type),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedUnitType = value!;
});
},
),
),
],
),
const SizedBox(height: 8),
Text(
'Total per intake: ${_dosageAmountController.text.isNotEmpty && _numberOfUnitsController.text.isNotEmpty ? (double.tryParse(_dosageAmountController.text) ?? 0) * (int.tryParse(_numberOfUnitsController.text) ?? 0) : 0} $_selectedUnit',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 16),
// Frequency per day
Row(
children: [
const Text('Frequency per day: '),
const SizedBox(width: 8),
DropdownButton<int>(
value: _frequencyPerDay,
items: List.generate(6, (index) => index + 1).map((freq) {
return DropdownMenuItem(
value: freq,
child: Text('$freq time${freq > 1 ? 's' : ''}'),
);
}).toList(),
onChanged: (value) {
setState(() {
_frequencyPerDay = value!;
_adjustReminderTimes();
});
},
),
],
),
const SizedBox(height: 16),
// Reminder times
const Text(
'Reminder Times',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
..._reminderTimes.asMap().entries.map((entry) {
final index = entry.key;
final time = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Expanded(
child: InkWell(
onTap: () => _selectTime(index),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(4),
),
child: Text(time),
),
),
),
if (_reminderTimes.length > 1)
IconButton(
onPressed: () => _removeReminderTime(index),
icon: const Icon(Icons.remove_circle_outline),
),
],
),
);
}),
if (_reminderTimes.length < _frequencyPerDay)
TextButton.icon(
onPressed: _addReminderTime,
icon: const Icon(Icons.add),
label: const Text('Add Reminder Time'),
),
const SizedBox(height: 16),
// Notes
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes (optional)',
border: OutlineInputBorder(),
hintText: 'Take with food, before meal, etc.',
),
maxLines: 3,
),
const SizedBox(height: 24),
// Save button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveSupplement,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
child: Text(isEditing ? 'Update Supplement' : 'Add Supplement'),
),
),
],
),
),
),
);
}
void _adjustReminderTimes() {
if (_reminderTimes.length > _frequencyPerDay) {
_reminderTimes = _reminderTimes.take(_frequencyPerDay).toList();
} else if (_reminderTimes.length < _frequencyPerDay) {
while (_reminderTimes.length < _frequencyPerDay) {
_reminderTimes.add('08:00');
}
}
}
void _selectTime(int index) {
DatePicker.showTimePicker(
context,
showTitleActions: true,
onConfirm: (time) {
setState(() {
_reminderTimes[index] = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
});
},
currentTime: DateTime.now(),
);
}
void _addReminderTime() {
if (_reminderTimes.length < _frequencyPerDay) {
setState(() {
_reminderTimes.add('08:00');
});
}
}
void _removeReminderTime(int index) {
if (_reminderTimes.length > 1) {
setState(() {
_reminderTimes.removeAt(index);
});
}
}
void _saveSupplement() async {
if (_formKey.currentState!.validate()) {
final supplement = Supplement(
id: widget.supplement?.id,
name: _nameController.text.trim(),
dosageAmount: double.parse(_dosageAmountController.text),
numberOfUnits: int.parse(_numberOfUnitsController.text),
unit: _selectedUnit,
unitType: _selectedUnitType,
frequencyPerDay: _frequencyPerDay,
reminderTimes: _reminderTimes,
notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null,
createdAt: widget.supplement?.createdAt ?? DateTime.now(),
);
final provider = context.read<SupplementProvider>();
try {
if (widget.supplement != null) {
await provider.updateSupplement(supplement);
} else {
await provider.addSupplement(supplement);
}
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.supplement != null
? 'Supplement updated successfully!'
: 'Supplement added successfully!'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
}
@override
void dispose() {
_nameController.dispose();
_dosageAmountController.dispose();
_numberOfUnitsController.dispose();
_notesController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/supplement_provider.dart';
class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
DateTime _selectedDate = DateTime.now();
int _selectedMonth = DateTime.now().month;
int _selectedYear = DateTime.now().year;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Intake History'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Daily View'),
Tab(text: 'Monthly View'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildDailyView(),
_buildMonthlyView(),
],
),
);
}
Widget _buildDailyView() {
return Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
setState(() {
_selectedDate = _selectedDate.subtract(const Duration(days: 1));
});
},
icon: const Icon(Icons.chevron_left),
),
InkWell(
onTap: _selectDate,
child: Text(
DateFormat('EEEE, MMM d, yyyy').format(_selectedDate),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
onPressed: _selectedDate.isBefore(DateTime.now())
? () {
setState(() {
_selectedDate = _selectedDate.add(const Duration(days: 1));
});
}
: null,
icon: const Icon(Icons.chevron_right),
),
],
),
),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: context.read<SupplementProvider>().getIntakesForDate(_selectedDate),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event_note,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No supplements taken on this day',
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
final intakes = snapshot.data!;
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: intakes.length,
itemBuilder: (context, index) {
final intake = intakes[index];
final takenAt = DateTime.parse(intake['takenAt']);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary),
),
title: Text(intake['supplementName']),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${intake['dosageTaken']} ${intake['supplementUnit']}'),
Text(
'Taken at ${DateFormat('HH:mm').format(takenAt)}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty)
Text(
intake['notes'],
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
],
),
trailing: PopupMenuButton(
onSelected: (value) {
if (value == 'delete') {
_deleteIntake(context, intake['id'], intake['supplementName']);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
),
),
);
},
);
},
),
),
],
);
}
Widget _buildMonthlyView() {
return Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
setState(() {
if (_selectedMonth == 1) {
_selectedMonth = 12;
_selectedYear--;
} else {
_selectedMonth--;
}
});
context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth);
},
icon: const Icon(Icons.chevron_left),
),
Text(
DateFormat('MMMM yyyy').format(DateTime(_selectedYear, _selectedMonth)),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () {
final now = DateTime.now();
if (_selectedYear < now.year || (_selectedYear == now.year && _selectedMonth < now.month)) {
setState(() {
if (_selectedMonth == 12) {
_selectedMonth = 1;
_selectedYear++;
} else {
_selectedMonth++;
}
});
context.read<SupplementProvider>().loadMonthlyIntakes(_selectedYear, _selectedMonth);
}
},
icon: const Icon(Icons.chevron_right),
),
],
),
),
Expanded(
child: Consumer<SupplementProvider>(
builder: (context, provider, child) {
if (provider.monthlyIntakes.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_month,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
'No supplements taken this month',
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
// Group intakes by date
final groupedIntakes = <String, List<Map<String, dynamic>>>{};
for (final intake in provider.monthlyIntakes) {
final date = DateTime.parse(intake['takenAt']);
final dateKey = DateFormat('yyyy-MM-dd').format(date);
groupedIntakes.putIfAbsent(dateKey, () => []);
groupedIntakes[dateKey]!.add(intake);
}
final sortedDates = groupedIntakes.keys.toList()
..sort((a, b) => b.compareTo(a)); // Most recent first
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sortedDates.length,
itemBuilder: (context, index) {
final dateKey = sortedDates[index];
final dayIntakes = groupedIntakes[dateKey]!;
final date = DateTime.parse(dateKey);
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('EEEE, MMM d').format(date),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...dayIntakes.map((intake) {
final takenAt = DateTime.parse(intake['takenAt']);
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Icon(
Icons.circle,
size: 8,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${intake['supplementName']} - ${intake['dosageTaken']} ${intake['supplementUnit']} at ${DateFormat('HH:mm').format(takenAt)}',
style: const TextStyle(fontSize: 14),
),
),
],
),
);
}),
const SizedBox(height: 4),
Text(
'${dayIntakes.length} supplement${dayIntakes.length != 1 ? 's' : ''} taken',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
},
);
},
),
),
],
);
}
void _selectDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedDate = picked;
});
}
}
void _deleteIntake(BuildContext context, int intakeId, String supplementName) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Intake'),
content: Text('Are you sure you want to delete this $supplementName intake?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
context.read<SupplementProvider>().deleteIntake(intakeId);
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Intake deleted'),
backgroundColor: Colors.red,
),
);
setState(() {}); // Refresh the view
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Delete'),
),
],
),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/supplement_provider.dart';
import 'supplements_list_screen.dart';
import 'history_screen.dart';
import 'add_supplement_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const SupplementsListScreen(),
const HistoryScreen(),
];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SupplementProvider>().initialize();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.medication),
label: 'Supplements',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: 'History',
),
],
),
floatingActionButton: _currentIndex == 0
? FloatingActionButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const AddSupplementScreen(),
),
);
// Refresh the list when returning from add screen
if (context.mounted) {
context.read<SupplementProvider>().loadSupplements();
}
},
child: const Icon(Icons.add),
)
: null,
);
}
}

View File

@@ -0,0 +1,268 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/supplement_provider.dart';
import '../models/supplement.dart';
import '../widgets/supplement_card.dart';
import 'add_supplement_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,
),
body: Consumer<SupplementProvider>(
builder: (context, provider, 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();
},
child: Column(
children: [
// Today's Intakes Section
if (provider.todayIntakes.isNotEmpty) ...[
Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.check_circle, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text(
'Today\'s Intakes',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
const SizedBox(height: 8),
...provider.todayIntakes.map((intake) {
final takenAt = DateTime.parse(intake['takenAt']);
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'${intake['supplementName']} - ${intake['dosageTaken']} ${intake['supplementUnit']} at ${DateFormat('HH:mm').format(takenAt)}',
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer),
),
);
}),
],
),
),
],
// Supplements List
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.supplements.length,
itemBuilder: (context, index) {
final supplement = provider.supplements[index];
return SupplementCard(
supplement: supplement,
onTake: () => _showTakeDialog(context, supplement),
onEdit: () => _editSupplement(context, supplement),
onDelete: () => _deleteSupplement(context, supplement),
);
},
),
),
],
),
);
},
),
);
}
void _showTakeDialog(BuildContext context, Supplement supplement) {
final unitsController = TextEditingController(text: supplement.numberOfUnits.toString());
final notesController = TextEditingController();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
final units = int.tryParse(unitsController.text) ?? supplement.numberOfUnits;
final totalDosage = supplement.dosageAmount * units;
return AlertDialog(
title: Text('Take ${supplement.name}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: TextField(
controller: unitsController,
keyboardType: TextInputType.number,
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(
'${totalDosage.toStringAsFixed(1)} ${supplement.unit}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
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 = int.tryParse(unitsController.text) ?? supplement.numberOfUnits;
final totalDosageTaken = supplement.dosageAmount * unitsTaken;
context.read<SupplementProvider>().recordIntake(
supplement.id!,
totalDosageTaken,
unitsTaken: unitsTaken,
notes: notesController.text.isNotEmpty ? notesController.text : null,
);
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${supplement.name} recorded!'),
backgroundColor: Colors.green,
),
);
},
child: const Text('Take'),
),
],
);
},
),
);
}
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'),
),
],
),
);
}
}

View File

@@ -0,0 +1,221 @@
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart';
import 'dart:io';
import '../models/supplement.dart';
import '../models/supplement_intake.dart';
class DatabaseHelper {
static const _databaseName = 'supplements.db';
static const _databaseVersion = 2; // Increment version for schema changes
static const supplementsTable = 'supplements';
static const intakesTable = 'supplement_intakes';
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
static bool _initialized = false;
static void _initializeDatabaseFactory() {
if (!_initialized) {
// Initialize for desktop platforms
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
_initialized = true;
}
}
Future<Database> get database async {
_initializeDatabaseFactory();
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $supplementsTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dosageAmount REAL NOT NULL,
numberOfUnits INTEGER NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
unitType TEXT NOT NULL DEFAULT 'units',
frequencyPerDay INTEGER NOT NULL,
reminderTimes TEXT NOT NULL,
notes TEXT,
createdAt TEXT NOT NULL,
isActive INTEGER NOT NULL DEFAULT 1
)
''');
await db.execute('''
CREATE TABLE $intakesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplementId INTEGER NOT NULL,
takenAt TEXT NOT NULL,
dosageTaken REAL NOT NULL,
unitsTaken INTEGER NOT NULL DEFAULT 1,
notes TEXT,
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add new columns for version 2
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN numberOfUnits INTEGER DEFAULT 1');
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken INTEGER DEFAULT 1');
// Migrate existing data
await db.execute('''
UPDATE $supplementsTable
SET dosageAmount = dosage,
numberOfUnits = 1,
unitType = 'units'
WHERE dosageAmount = 0
''');
}
}
// Supplement CRUD operations
Future<int> insertSupplement(Supplement supplement) async {
Database db = await database;
return await db.insert(supplementsTable, supplement.toMap());
}
Future<List<Supplement>> getAllSupplements() async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'isActive = ?',
whereArgs: [1],
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Supplement.fromMap(maps[i]));
}
Future<Supplement?> getSupplement(int id) async {
Database db = await database;
List<Map<String, dynamic>> maps = await db.query(
supplementsTable,
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Supplement.fromMap(maps.first);
}
return null;
}
Future<int> updateSupplement(Supplement supplement) async {
Database db = await database;
return await db.update(
supplementsTable,
supplement.toMap(),
where: 'id = ?',
whereArgs: [supplement.id],
);
}
Future<int> deleteSupplement(int id) async {
Database db = await database;
return await db.update(
supplementsTable,
{'isActive': 0},
where: 'id = ?',
whereArgs: [id],
);
}
// Supplement Intake CRUD operations
Future<int> insertIntake(SupplementIntake intake) async {
Database db = await database;
return await db.insert(intakesTable, intake.toMap());
}
Future<List<SupplementIntake>> getIntakesForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<SupplementIntake>> getIntakesForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> maps = await db.query(
intakesTable,
where: 'takenAt >= ? AND takenAt <= ?',
whereArgs: [startDate, endDate],
orderBy: 'takenAt DESC',
);
return List.generate(maps.length, (i) => SupplementIntake.fromMap(maps[i]));
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForDate(DateTime date) async {
Database db = await database;
String startDate = DateTime(date.year, date.month, date.day).toIso8601String();
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<List<Map<String, dynamic>>> getIntakesWithSupplementsForMonth(int year, int month) async {
Database db = await database;
String startDate = DateTime(year, month, 1).toIso8601String();
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
FROM $intakesTable i
JOIN $supplementsTable s ON i.supplementId = s.id
WHERE i.takenAt >= ? AND i.takenAt <= ?
ORDER BY i.takenAt DESC
''', [startDate, endDate]);
return result;
}
Future<int> deleteIntake(int id) async {
Database db = await database;
return await db.delete(
intakesTable,
where: 'id = ?',
whereArgs: [id],
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
import '../models/supplement.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
tz.initializeTimeZones();
const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notifications.initialize(initSettings);
}
Future<bool> requestPermissions() async {
final androidPlugin = _notifications.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
if (androidPlugin != null) {
await androidPlugin.requestNotificationsPermission();
}
final iosPlugin = _notifications.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
if (iosPlugin != null) {
await iosPlugin.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
return true;
}
Future<void> scheduleSupplementReminders(Supplement supplement) async {
// Cancel existing notifications for this supplement
await cancelSupplementReminders(supplement.id!);
for (int i = 0; i < supplement.reminderTimes.length; i++) {
final timeStr = supplement.reminderTimes[i];
final timeParts = timeStr.split(':');
final hour = int.parse(timeParts[0]);
final minute = int.parse(timeParts[1]);
final notificationId = supplement.id! * 100 + i; // Unique ID for each reminder
await _notifications.zonedSchedule(
notificationId,
'Time for ${supplement.name}',
'Take ${supplement.dosage} ${supplement.unit}',
_nextInstanceOfTime(hour, minute),
const NotificationDetails(
android: AndroidNotificationDetails(
'supplement_reminders',
'Supplement Reminders',
channelDescription: 'Notifications for supplement intake reminders',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
}
}
Future<void> cancelSupplementReminders(int supplementId) async {
// Cancel all notifications for this supplement (up to 10 possible reminders)
for (int i = 0; i < 10; i++) {
final notificationId = supplementId * 100 + i;
await _notifications.cancel(notificationId);
}
}
Future<void> cancelAllReminders() async {
await _notifications.cancelAll();
}
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
Future<void> showInstantNotification(String title, String body) async {
const NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
'instant_notifications',
'Instant Notifications',
channelDescription: 'Instant notifications for supplement app',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
);
await _notifications.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
notificationDetails,
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import '../models/supplement.dart';
class SupplementCard extends StatelessWidget {
final Supplement supplement;
final VoidCallback onTake;
final VoidCallback onEdit;
final VoidCallback onDelete;
const SupplementCard({
super.key,
required this.supplement,
required this.onTake,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
supplement.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${supplement.numberOfUnits} ${supplement.unitType} (${supplement.dosageAmount} ${supplement.unit} each)',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
PopupMenuButton(
onSelected: (value) {
switch (value) {
case 'edit':
onEdit();
break;
case 'delete':
onDelete();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.schedule, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(
'${supplement.frequencyPerDay}x daily',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 16),
Icon(Icons.notifications, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 4),
Text(
supplement.reminderTimes.join(', '),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
if (supplement.notes != null && supplement.notes!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
supplement.notes!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
],
const SizedBox(height: 12),
// Take supplement section
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Take Supplement',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Row(
children: [
Text(
'Amount: ',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
'${supplement.numberOfUnits} ${supplement.unitType}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
Text(
' (${supplement.totalDosagePerIntake} ${supplement.unit})',
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
ElevatedButton.icon(
onPressed: onTake,
icon: const Icon(Icons.medication, size: 16),
label: const Text('Take', style: TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
minimumSize: const Size(80, 32),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
),
],
),
],
),
),
],
),
),
);
}
}