Skip to content
85 changes: 72 additions & 13 deletions app/lib/database/database_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,24 @@ class DatabaseHelper {
)
''');

// Budgets table
await db.execute('''
CREATE TABLE budgets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
amount REAL NOT NULL,
categoryId INTEGER,
startDate TEXT NOT NULL,
endDate TEXT,
rollover INTEGER NOT NULL DEFAULT 0,
alertThreshold REAL NOT NULL DEFAULT 80.0,
isActive INTEGER NOT NULL DEFAULT 1,
createdAt TEXT NOT NULL,
updatedAt TEXT
)
''');

// Receiver category mappings table
await db.execute('''
CREATE TABLE receiver_category_mappings (
Expand Down Expand Up @@ -189,8 +207,14 @@ class DatabaseHelper {
await db.execute('CREATE INDEX idx_accounts_bank ON accounts(bank)');
await db.execute(
'CREATE INDEX idx_accounts_accountNumber ON accounts(accountNumber)');
await db.execute(
'CREATE INDEX idx_accounts_profileId ON accounts(profileId)');
await db.execute('CREATE INDEX idx_budgets_type ON budgets(type)');
await db
.execute('CREATE INDEX idx_budgets_categoryId ON budgets(categoryId)');
await db.execute('CREATE INDEX idx_budgets_isActive ON budgets(isActive)');
await db
.execute('CREATE INDEX idx_budgets_startDate ON budgets(startDate)');
await db
.execute('CREATE INDEX idx_accounts_profileId ON accounts(profileId)');
await db.execute(
'CREATE INDEX idx_transactions_profileId ON transactions(profileId)');

Expand Down Expand Up @@ -443,6 +467,37 @@ class DatabaseHelper {
}

if (oldVersion < 14) {
// Add budgets table for version 14
try {
await db.execute('''
CREATE TABLE IF NOT EXISTS budgets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
amount REAL NOT NULL,
categoryId INTEGER,
startDate TEXT NOT NULL,
endDate TEXT,
rollover INTEGER NOT NULL DEFAULT 0,
alertThreshold REAL NOT NULL DEFAULT 80.0,
isActive INTEGER NOT NULL DEFAULT 1,
createdAt TEXT NOT NULL,
updatedAt TEXT
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_budgets_type ON budgets(type)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_budgets_categoryId ON budgets(categoryId)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_budgets_isActive ON budgets(isActive)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_budgets_startDate ON budgets(startDate)');
print("debug: Added budgets table");
} catch (e) {
print("debug: Error adding budgets table (might already exist): $e");
}

// Add receiver category mappings table for version 14
try {
await db.execute('''
Expand All @@ -463,7 +518,8 @@ class DatabaseHelper {
);
print("debug: Added receiver_category_mappings table");
} catch (e) {
print("debug: Error adding receiver_category_mappings table (might already exist): $e");
print(
"debug: Error adding receiver_category_mappings table (might already exist): $e");
}
}

Expand All @@ -473,11 +529,12 @@ class DatabaseHelper {
// Add profileId to accounts table
await db.execute('ALTER TABLE accounts ADD COLUMN profileId INTEGER');
print("debug: Added profileId column to accounts table");

// Add profileId to transactions table
await db.execute('ALTER TABLE transactions ADD COLUMN profileId INTEGER');
await db
.execute('ALTER TABLE transactions ADD COLUMN profileId INTEGER');
print("debug: Added profileId column to transactions table");

// Create indexes for better query performance
await db.execute(
"CREATE INDEX IF NOT EXISTS idx_accounts_profileId ON accounts(profileId)",
Expand All @@ -486,21 +543,21 @@ class DatabaseHelper {
"CREATE INDEX IF NOT EXISTS idx_transactions_profileId ON transactions(profileId)",
);
print("debug: Created indexes for profileId columns");

// Migrate existing data: assign all existing accounts/transactions to active profile
// Get active profile ID (or first profile if none active)
// Access SharedPreferences directly to avoid circular dependency during migration
final prefs = await SharedPreferences.getInstance();
int? activeProfileId = prefs.getInt('active_profile_id');

if (activeProfileId == null) {
// Get first profile from database
final profileResult = await db.query(
'profiles',
orderBy: 'createdAt ASC',
limit: 1,
);

if (profileResult.isNotEmpty) {
activeProfileId = profileResult.first['id'] as int?;
if (activeProfileId != null) {
Expand All @@ -519,24 +576,26 @@ class DatabaseHelper {
await prefs.setInt('active_profile_id', activeProfileId);
}
}

// Update all existing accounts to use active profile
await db.update(
'accounts',
{'profileId': activeProfileId},
where: 'profileId IS NULL',
);
print("debug: Migrated existing accounts to profile $activeProfileId");

// Update all existing transactions to use active profile
await db.update(
'transactions',
{'profileId': activeProfileId},
where: 'profileId IS NULL',
);
print("debug: Migrated existing transactions to profile $activeProfileId");
print(
"debug: Migrated existing transactions to profile $activeProfileId");
} catch (e) {
print("debug: Error adding profileId columns (might already exist): $e");
print(
"debug: Error adding profileId columns (might already exist): $e");
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:totals/providers/insights_provider.dart';
import 'package:totals/providers/theme_provider.dart';
import 'package:totals/providers/transaction_provider.dart';
import 'package:totals/providers/budget_provider.dart';
import 'package:totals/screens/home_page.dart';
import 'package:totals/database/migration_helper.dart';
import 'package:totals/services/account_sync_status_service.dart';
Expand Down Expand Up @@ -50,6 +51,7 @@ class MyApp extends StatelessWidget {
providers: [
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => TransactionProvider()),
ChangeNotifierProvider(create: (_) => BudgetProvider()),

// we need insights provider to use the existing transacton provider instead of using
// a new transaction provider instance.
Expand Down
144 changes: 144 additions & 0 deletions app/lib/models/budget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
class Budget {
final int? id;
final String name;
final String type; // 'daily', 'monthly', 'yearly', 'category'
final double amount;
final int? categoryId;
final DateTime startDate;
final DateTime? endDate;
final bool rollover;
final double alertThreshold; // 0-100 percentage
final bool isActive;
final DateTime createdAt;
final DateTime? updatedAt;

Budget({
this.id,
required this.name,
required this.type,
required this.amount,
this.categoryId,
required this.startDate,
this.endDate,
this.rollover = false,
this.alertThreshold = 80.0,
this.isActive = true,
required this.createdAt,
this.updatedAt,
});

factory Budget.fromDb(Map<String, dynamic> row) {
return Budget(
id: row['id'] as int?,
name: (row['name'] as String?) ?? '',
type: (row['type'] as String?) ?? 'monthly',
amount: (row['amount'] as num?)?.toDouble() ?? 0.0,
categoryId: row['categoryId'] as int?,
startDate: row['startDate'] != null
? DateTime.parse(row['startDate'] as String)
: DateTime.now(),
endDate: row['endDate'] != null
? DateTime.parse(row['endDate'] as String)
: null,
rollover: (row['rollover'] as int? ?? 0) == 1,
alertThreshold: (row['alertThreshold'] as num?)?.toDouble() ?? 80.0,
isActive: (row['isActive'] as int? ?? 1) == 1,
createdAt: row['createdAt'] != null
? DateTime.parse(row['createdAt'] as String)
: DateTime.now(),
updatedAt: row['updatedAt'] != null
? DateTime.parse(row['updatedAt'] as String)
: null,
);
}

Map<String, dynamic> toDb() {
return {
'id': id,
'name': name,
'type': type,
'amount': amount,
'categoryId': categoryId,
'startDate': startDate.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'rollover': rollover ? 1 : 0,
'alertThreshold': alertThreshold,
'isActive': isActive ? 1 : 0,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}

Budget copyWith({
int? id,
String? name,
String? type,
double? amount,
int? categoryId,
DateTime? startDate,
DateTime? endDate,
bool? rollover,
double? alertThreshold,
bool? isActive,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Budget(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
amount: amount ?? this.amount,
categoryId: categoryId ?? this.categoryId,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
rollover: rollover ?? this.rollover,
alertThreshold: alertThreshold ?? this.alertThreshold,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}

// Helper methods for period calculations
DateTime getCurrentPeriodStart() {
final now = DateTime.now();
switch (type) {
case 'daily':
return DateTime(now.year, now.month, now.day);
case 'monthly':
return DateTime(now.year, now.month, 1);
case 'yearly':
return DateTime(now.year, 1, 1);
case 'category':
// For category budgets, use monthly by default
return DateTime(now.year, now.month, 1);
default:
return DateTime(now.year, now.month, 1);
}
}

DateTime getCurrentPeriodEnd() {
final start = getCurrentPeriodStart();
switch (type) {
case 'daily':
return DateTime(start.year, start.month, start.day, 23, 59, 59);
case 'monthly':
final nextMonth = DateTime(start.year, start.month + 1, 1);
return nextMonth.subtract(const Duration(seconds: 1));
case 'yearly':
return DateTime(start.year, 12, 31, 23, 59, 59);
case 'category':
final nextMonth = DateTime(start.year, start.month + 1, 1);
return nextMonth.subtract(const Duration(seconds: 1));
default:
final nextMonth = DateTime(start.year, start.month + 1, 1);
return nextMonth.subtract(const Duration(seconds: 1));
}
}

bool isDateInCurrentPeriod(DateTime date) {
final start = getCurrentPeriodStart();
final end = getCurrentPeriodEnd();
return !date.isBefore(start) && !date.isAfter(end);
}
}
Loading