mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
Refactor supplement intake handling to support fractional units and update notification service initialization for Linux
This commit is contained in:
@@ -33,8 +33,12 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Notification receiver for local notifications -->
|
<!-- Notification receiver for local notifications -->
|
||||||
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
<receiver
|
||||||
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
<receiver
|
||||||
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||||
|
@@ -3,7 +3,7 @@ class SupplementIntake {
|
|||||||
final int supplementId;
|
final int supplementId;
|
||||||
final DateTime takenAt;
|
final DateTime takenAt;
|
||||||
final double dosageTaken; // Total dosage amount taken
|
final double dosageTaken; // Total dosage amount taken
|
||||||
final int unitsTaken; // Number of units taken
|
final double unitsTaken; // Number of units taken (can be fractional)
|
||||||
final String? notes;
|
final String? notes;
|
||||||
|
|
||||||
SupplementIntake({
|
SupplementIntake({
|
||||||
@@ -32,7 +32,7 @@ class SupplementIntake {
|
|||||||
supplementId: map['supplementId'],
|
supplementId: map['supplementId'],
|
||||||
takenAt: DateTime.parse(map['takenAt']),
|
takenAt: DateTime.parse(map['takenAt']),
|
||||||
dosageTaken: map['dosageTaken'],
|
dosageTaken: map['dosageTaken'],
|
||||||
unitsTaken: map['unitsTaken'] ?? 1, // Default for backwards compatibility
|
unitsTaken: (map['unitsTaken'] ?? 1).toDouble(), // Default for backwards compatibility
|
||||||
notes: map['notes'],
|
notes: map['notes'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ class SupplementIntake {
|
|||||||
int? supplementId,
|
int? supplementId,
|
||||||
DateTime? takenAt,
|
DateTime? takenAt,
|
||||||
double? dosageTaken,
|
double? dosageTaken,
|
||||||
int? unitsTaken,
|
double? unitsTaken,
|
||||||
String? notes,
|
String? notes,
|
||||||
}) {
|
}) {
|
||||||
return SupplementIntake(
|
return SupplementIntake(
|
||||||
|
@@ -103,13 +103,13 @@ class SupplementProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> recordIntake(int supplementId, double dosage, {int? unitsTaken, String? notes}) async {
|
Future<void> recordIntake(int supplementId, double dosage, {double? unitsTaken, String? notes}) async {
|
||||||
try {
|
try {
|
||||||
final intake = SupplementIntake(
|
final intake = SupplementIntake(
|
||||||
supplementId: supplementId,
|
supplementId: supplementId,
|
||||||
takenAt: DateTime.now(),
|
takenAt: DateTime.now(),
|
||||||
dosageTaken: dosage,
|
dosageTaken: dosage,
|
||||||
unitsTaken: unitsTaken ?? 1,
|
unitsTaken: unitsTaken ?? 1.0,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class SupplementProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Show confirmation notification
|
// Show confirmation notification
|
||||||
final supplement = _supplements.firstWhere((s) => s.id == supplementId);
|
final supplement = _supplements.firstWhere((s) => s.id == supplementId);
|
||||||
final unitsText = unitsTaken != null && unitsTaken > 1 ? '$unitsTaken ${supplement.unitType}' : '';
|
final unitsText = unitsTaken != null && unitsTaken != 1 ? '${unitsTaken.toStringAsFixed(unitsTaken % 1 == 0 ? 0 : 1)} ${supplement.unitType}' : '';
|
||||||
await _notificationService.showInstantNotification(
|
await _notificationService.showInstantNotification(
|
||||||
'Supplement Taken',
|
'Supplement Taken',
|
||||||
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})',
|
'Recorded ${supplement.name}${unitsText.isNotEmpty ? ' - $unitsText' : ''} ($dosage ${supplement.unit})',
|
||||||
|
@@ -120,62 +120,98 @@ class _HistoryScreenState extends State<HistoryScreen> with SingleTickerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
final intakes = snapshot.data!;
|
final intakes = snapshot.data!;
|
||||||
|
|
||||||
|
// Group intakes by supplement
|
||||||
|
final Map<String, List<Map<String, dynamic>>> groupedIntakes = {};
|
||||||
|
for (final intake in intakes) {
|
||||||
|
final supplementName = intake['supplementName'] as String;
|
||||||
|
groupedIntakes.putIfAbsent(supplementName, () => []);
|
||||||
|
groupedIntakes[supplementName]!.add(intake);
|
||||||
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: intakes.length,
|
itemCount: groupedIntakes.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final intake = intakes[index];
|
final supplementName = groupedIntakes.keys.elementAt(index);
|
||||||
final takenAt = DateTime.parse(intake['takenAt']);
|
final supplementIntakes = groupedIntakes[supplementName]!;
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
double totalDosage = 0;
|
||||||
|
double totalUnits = 0;
|
||||||
|
final firstIntake = supplementIntakes.first;
|
||||||
|
|
||||||
|
for (final intake in supplementIntakes) {
|
||||||
|
totalDosage += intake['dosageTaken'] as double;
|
||||||
|
totalUnits += (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
child: ListTile(
|
child: ExpansionTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary),
|
child: Icon(Icons.medication, color: Theme.of(context).colorScheme.onPrimary),
|
||||||
),
|
),
|
||||||
title: Text(intake['supplementName']),
|
title: Text(
|
||||||
|
supplementName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('${intake['dosageTaken']} ${intake['supplementUnit']}'),
|
|
||||||
Text(
|
Text(
|
||||||
'Taken at ${DateFormat('HH:mm').format(takenAt)}',
|
'${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnit']} total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${totalUnits.toStringAsFixed(totalUnits % 1 == 0 ? 0 : 1)} ${firstIntake['supplementUnitType'] ?? 'units'} • ${supplementIntakes.length} intake${supplementIntakes.length > 1 ? 's' : ''}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
children: supplementIntakes.map((intake) {
|
||||||
|
final takenAt = DateTime.parse(intake['takenAt']);
|
||||||
|
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.only(left: 72, right: 16),
|
||||||
|
title: Text(
|
||||||
|
'${(intake['dosageTaken'] as double).toStringAsFixed((intake['dosageTaken'] as double) % 1 == 0 ? 0 : 1)} ${intake['supplementUnit']}',
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty)
|
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty)
|
||||||
Text(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Text(
|
||||||
intake['notes'],
|
intake['notes'],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: PopupMenuButton(
|
);
|
||||||
onSelected: (value) {
|
}).toList(),
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -133,7 +133,7 @@ class SupplementsListScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
final units = int.tryParse(unitsController.text) ?? supplement.numberOfUnits;
|
final units = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
|
||||||
final totalDosage = supplement.dosageAmount * units;
|
final totalDosage = supplement.dosageAmount * units;
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
@@ -146,7 +146,7 @@ class SupplementsListScreen extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: unitsController,
|
controller: unitsController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Number of ${supplement.unitType}',
|
labelText: 'Number of ${supplement.unitType}',
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
@@ -175,7 +175,7 @@ class SupplementsListScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${totalDosage.toStringAsFixed(1)} ${supplement.unit}',
|
'${totalDosage.toStringAsFixed(totalDosage % 1 == 0 ? 0 : 1)} ${supplement.unit}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -203,7 +203,7 @@ class SupplementsListScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final unitsTaken = int.tryParse(unitsController.text) ?? supplement.numberOfUnits;
|
final unitsTaken = double.tryParse(unitsController.text) ?? supplement.numberOfUnits.toDouble();
|
||||||
final totalDosageTaken = supplement.dosageAmount * unitsTaken;
|
final totalDosageTaken = supplement.dosageAmount * unitsTaken;
|
||||||
context.read<SupplementProvider>().recordIntake(
|
context.read<SupplementProvider>().recordIntake(
|
||||||
supplement.id!,
|
supplement.id!,
|
||||||
|
@@ -68,7 +68,7 @@ class DatabaseHelper {
|
|||||||
supplementId INTEGER NOT NULL,
|
supplementId INTEGER NOT NULL,
|
||||||
takenAt TEXT NOT NULL,
|
takenAt TEXT NOT NULL,
|
||||||
dosageTaken REAL NOT NULL,
|
dosageTaken REAL NOT NULL,
|
||||||
unitsTaken INTEGER NOT NULL DEFAULT 1,
|
unitsTaken REAL NOT NULL DEFAULT 1,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
|
FOREIGN KEY (supplementId) REFERENCES $supplementsTable (id)
|
||||||
)
|
)
|
||||||
@@ -77,20 +77,49 @@ class DatabaseHelper {
|
|||||||
|
|
||||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
// Add new columns for version 2
|
// First, add new columns
|
||||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN dosageAmount REAL DEFAULT 0');
|
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 numberOfUnits INTEGER DEFAULT 1');
|
||||||
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
|
await db.execute('ALTER TABLE $supplementsTable ADD COLUMN unitType TEXT DEFAULT "units"');
|
||||||
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken INTEGER DEFAULT 1');
|
await db.execute('ALTER TABLE $intakesTable ADD COLUMN unitsTaken REAL DEFAULT 1');
|
||||||
|
|
||||||
// Migrate existing data
|
// Migrate existing data from old dosage column to new dosageAmount column
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
UPDATE $supplementsTable
|
UPDATE $supplementsTable
|
||||||
SET dosageAmount = dosage,
|
SET dosageAmount = COALESCE(dosage, 0),
|
||||||
numberOfUnits = 1,
|
numberOfUnits = 1,
|
||||||
unitType = 'units'
|
unitType = 'units'
|
||||||
WHERE dosageAmount = 0
|
WHERE dosageAmount = 0
|
||||||
''');
|
''');
|
||||||
|
|
||||||
|
// Create new table with correct schema
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE ${supplementsTable}_new (
|
||||||
|
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
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
// Copy data to new table
|
||||||
|
await db.execute('''
|
||||||
|
INSERT INTO ${supplementsTable}_new
|
||||||
|
(id, name, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive)
|
||||||
|
SELECT id, name, dosageAmount, numberOfUnits, unit, unitType, frequencyPerDay, reminderTimes, notes, createdAt, isActive
|
||||||
|
FROM $supplementsTable
|
||||||
|
''');
|
||||||
|
|
||||||
|
// Drop old table and rename new table
|
||||||
|
await db.execute('DROP TABLE $supplementsTable');
|
||||||
|
await db.execute('ALTER TABLE ${supplementsTable}_new RENAME TO $supplementsTable');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +213,7 @@ class DatabaseHelper {
|
|||||||
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
|
String endDate = DateTime(date.year, date.month, date.day, 23, 59, 59).toIso8601String();
|
||||||
|
|
||||||
List<Map<String, dynamic>> result = await db.rawQuery('''
|
List<Map<String, dynamic>> result = await db.rawQuery('''
|
||||||
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
|
SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
|
||||||
FROM $intakesTable i
|
FROM $intakesTable i
|
||||||
JOIN $supplementsTable s ON i.supplementId = s.id
|
JOIN $supplementsTable s ON i.supplementId = s.id
|
||||||
WHERE i.takenAt >= ? AND i.takenAt <= ?
|
WHERE i.takenAt >= ? AND i.takenAt <= ?
|
||||||
@@ -200,7 +229,7 @@ class DatabaseHelper {
|
|||||||
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
|
String endDate = DateTime(year, month + 1, 0, 23, 59, 59).toIso8601String();
|
||||||
|
|
||||||
List<Map<String, dynamic>> result = await db.rawQuery('''
|
List<Map<String, dynamic>> result = await db.rawQuery('''
|
||||||
SELECT i.*, s.name as supplementName, s.unit as supplementUnit
|
SELECT i.*, s.name as supplementName, s.unit as supplementUnit, s.unitType as supplementUnitType
|
||||||
FROM $intakesTable i
|
FROM $intakesTable i
|
||||||
JOIN $supplementsTable s ON i.supplementId = s.id
|
JOIN $supplementsTable s ON i.supplementId = s.id
|
||||||
WHERE i.takenAt >= ? AND i.takenAt <= ?
|
WHERE i.takenAt >= ? AND i.takenAt <= ?
|
||||||
|
@@ -19,10 +19,14 @@ class NotificationService {
|
|||||||
requestBadgePermission: true,
|
requestBadgePermission: true,
|
||||||
requestSoundPermission: true,
|
requestSoundPermission: true,
|
||||||
);
|
);
|
||||||
|
const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings(
|
||||||
|
defaultActionName: 'Open notification',
|
||||||
|
);
|
||||||
|
|
||||||
const InitializationSettings initSettings = InitializationSettings(
|
const InitializationSettings initSettings = InitializationSettings(
|
||||||
android: androidSettings,
|
android: androidSettings,
|
||||||
iOS: iosSettings,
|
iOS: iosSettings,
|
||||||
|
linux: linuxSettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _notifications.initialize(initSettings);
|
await _notifications.initialize(initSettings);
|
||||||
|
Reference in New Issue
Block a user