mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
39
lib/main.dart
Normal file
39
lib/main.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
90
lib/models/supplement.dart
Normal file
90
lib/models/supplement.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
57
lib/models/supplement_intake.dart
Normal file
57
lib/models/supplement_intake.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
176
lib/providers/supplement_provider.dart
Normal file
176
lib/providers/supplement_provider.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
388
lib/screens/add_supplement_screen.dart
Normal file
388
lib/screens/add_supplement_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
394
lib/screens/history_screen.dart
Normal file
394
lib/screens/history_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
71
lib/screens/home_screen.dart
Normal file
71
lib/screens/home_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
268
lib/screens/supplements_list_screen.dart
Normal file
268
lib/screens/supplements_list_screen.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
221
lib/services/database_helper.dart
Normal file
221
lib/services/database_helper.dart
Normal 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],
|
||||
);
|
||||
}
|
||||
}
|
124
lib/services/notification_service.dart
Normal file
124
lib/services/notification_service.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
199
lib/widgets/supplement_card.dart
Normal file
199
lib/widgets/supplement_card.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user