Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 88 additions & 66 deletions ios/Runner.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions ios/Runner/AddWalletTransactionIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import AppIntents
import Foundation

@available(iOS 16.0, *)
struct AddWalletTransactionIntent: AppIntent {
static var title: LocalizedStringResource = "Add Wallet Transaction"
static var description = IntentDescription(
"Adds an uncategorized Apple Wallet transaction to Sossoldi."
)
static var openAppWhenRun = false

@Parameter(title: "Amount")
var amount: String

@Parameter(title: "Merchant")
var merchant: String

@Parameter(title: "Card")
var card: String

init() {}

init(amount: String, merchant: String, card: String) {
self.amount = amount
self.merchant = merchant
self.card = card
}

func perform() async throws -> some IntentResult {
_ = card
try PendingWalletTransactionStore.append(
amount: amount,
merchant: merchant.trimmingCharacters(in: .whitespacesAndNewlines),
card: card.trimmingCharacters(in: .whitespacesAndNewlines)
)
return .result()
}
}

private struct PendingWalletTransaction: Encodable {
let amount: String
let merchant: String
let card: String
let createdAt: String
}

private enum PendingWalletTransactionStore {
private static let fileName = "pending_wallet_transactions.jsonl"

static func append(amount: String, merchant: String, card: String) throws {
let pendingTransaction = PendingWalletTransaction(
amount: amount,
merchant: merchant,
card: card,
createdAt: iso8601Now()
)

let data = try JSONEncoder().encode(pendingTransaction) + Data("\n".utf8)
let url = try pendingTransactionsURL()

if FileManager.default.fileExists(atPath: url.path) {
let handle = try FileHandle(forWritingTo: url)
try handle.seekToEnd()
try handle.write(contentsOf: data)
try handle.close()
} else {
try data.write(to: url, options: .atomic)
}
}

private static func pendingTransactionsURL() throws -> URL {
let documentsURL = try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
return documentsURL.appendingPathComponent(fileName)
}

private static func iso8601Now() -> String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.string(from: Date())
}
}
34 changes: 34 additions & 0 deletions lib/pages/structure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers/settings_provider.dart';
import '../providers/transactions_provider.dart';
import '../services/wallet/pending_wallet_transaction_importer.dart';
import '../ui/device.dart';
import 'graphs/graphs_page.dart';
import 'dashboard/dashboard_page.dart';
Expand Down Expand Up @@ -36,6 +37,26 @@ class _StructureState extends ConsumerState<Structure> {
];

int selectedIndex = 0;
late final _AppResumeObserver _resumeObserver;

@override
void initState() {
super.initState();
_resumeObserver = _AppResumeObserver(_refreshTransactions);
WidgetsBinding.instance.addObserver(_resumeObserver);
_refreshTransactions();
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(_resumeObserver);
super.dispose();
}

void _refreshTransactions() async {
await PendingWalletTransactionImporter.importPending(ref);
ref.read(transactionsProvider.notifier).filterTransactions();
}

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -139,3 +160,16 @@ class _StructureState extends ConsumerState<Structure> {
);
}
}

class _AppResumeObserver extends WidgetsBindingObserver {
_AppResumeObserver(this.onResumed);

final VoidCallback onResumed;

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
onResumed();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,11 @@ class _CreateTransactionPage extends ConsumerState<CreateTransactionPage> {
}

void _refreshAccountAndNavigateBack() async {
ref
.read(accountsProvider.notifier)
.refreshAccount(ref.read(selectedBankAccountProvider)!)
.whenComplete(() {
if (mounted) Navigator.of(context).pop();
});
final selectedAccount = ref.read(selectedBankAccountProvider);
if (selectedAccount != null) {
await ref.read(accountsProvider.notifier).refreshAccount(selectedAccount);
}
if (mounted) Navigator.of(context).pop();
}

void _createOrUpdateTransaction() async {
Expand Down
16 changes: 6 additions & 10 deletions lib/pages/transactions/widgets/accounts_pie_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import 'package:fl_chart/fl_chart.dart';
import "package:flutter/material.dart";
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../constants/constants.dart';
import '../../../constants/style.dart';
import '../../../providers/transactions_provider.dart';
import '../../../ui/widgets/rounded_icon.dart';
import '../../../model/bank_account.dart';
import '../../../providers/currency_provider.dart';
import '../../../ui/device.dart';
import 'accounts_tab.dart';

class AccountsPieChart extends ConsumerWidget {
const AccountsPieChart({
Expand All @@ -18,7 +17,7 @@ class AccountsPieChart extends ConsumerWidget {
super.key,
});

final List<BankAccount> accounts;
final List<AccountEntry> accounts;
final Map<int, double> amounts;
final double total;

Expand All @@ -37,11 +36,11 @@ class AccountsPieChart extends ConsumerWidget {
centerSpaceRadius: 70,
sectionsSpace: 0,
borderData: FlBorderData(show: false),
sections: List.generate(amounts.values.length, (i) {
sections: List.generate(accounts.length, (i) {
final isTouched = (i == selectedIndex);
final radius = isTouched ? 30.0 : 25.0;
return PieChartSectionData(
color: accountColorList[accounts[i].color],
color: accounts[i].color,
value: 360 * amounts[accounts[i].id]!,
radius: radius,
showTitle: false,
Expand Down Expand Up @@ -69,11 +68,8 @@ class AccountsPieChart extends ConsumerWidget {
children: [
if (selectedIndex != -1)
RoundedIcon(
icon:
accountIconList[accounts[selectedIndex].symbol] ??
Icons.swap_horiz_rounded,
backgroundColor:
accountColorList[accounts[selectedIndex].color],
icon: accounts[selectedIndex].icon,
backgroundColor: accounts[selectedIndex].color,
padding: const EdgeInsets.all(Sizes.sm),
),
Text(
Expand Down
75 changes: 59 additions & 16 deletions lib/pages/transactions/widgets/accounts_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../constants/constants.dart';
import '../../../constants/style.dart';
import '../../../ui/widgets/default_container.dart';
import '../../../ui/widgets/transaction_type_button.dart';
import '../../../model/bank_account.dart';
Expand All @@ -15,6 +16,8 @@ import 'panel_list_tile.dart';
class AccountsTab extends ConsumerWidget {
const AccountsTab({super.key});

static const int _unassignedAccountId = 0;

@override
Widget build(BuildContext context, WidgetRef ref) {
final accounts = ref.watch(accountsProvider);
Expand Down Expand Up @@ -85,18 +88,14 @@ class AccountsTab extends ConsumerWidget {
const TransactionTypeButton(),
accounts.when(
data: (data) {
List<BankAccount> accountIncomeList = data
.where(
(account) =>
accountToAmountIncome.containsKey(account.id),
)
.toList();
List<BankAccount> accountExpenseList = data
.where(
(account) =>
accountToAmountExpense.containsKey(account.id),
)
.toList();
final accountIncomeList = _buildAccountEntries(
accounts: data,
amounts: accountToAmountIncome,
);
final accountExpenseList = _buildAccountEntries(
accounts: data,
amounts: accountToAmountExpense,
);
return transactionType == TransactionType.income
? accountIncomeList.isEmpty
? const SizedBox(
Expand Down Expand Up @@ -134,6 +133,50 @@ class AccountsTab extends ConsumerWidget {
),
);
}

List<AccountEntry> _buildAccountEntries({
required List<BankAccount> accounts,
required Map<int, double> amounts,
}) {
final entries = accounts
.where((account) => amounts.containsKey(account.id))
.map(
(account) => AccountEntry(
id: account.id!,
name: account.name,
icon: accountIconList[account.symbol],
color: accountColorList[account.color],
),
)
.toList();

if (amounts.containsKey(_unassignedAccountId)) {
entries.add(
const AccountEntry(
id: _unassignedAccountId,
name: 'Unassigned',
icon: Icons.account_balance_wallet_outlined,
color: grey2,
),
);
}

return entries;
}
}

class AccountEntry {
const AccountEntry({
required this.id,
required this.name,
required this.icon,
required this.color,
});

final int id;
final String name;
final IconData? icon;
final Color color;
}

class AccountSection extends StatelessWidget {
Expand All @@ -145,7 +188,7 @@ class AccountSection extends StatelessWidget {
super.key,
});

final List<BankAccount> accountList;
final List<AccountEntry> accountList;
final Map<int, double> amounts;
final double total;
final Map<int, List<Transaction>> transactions;
Expand All @@ -163,11 +206,11 @@ class AccountSection extends StatelessWidget {
separatorBuilder: (context, index) =>
const SizedBox(height: Sizes.sm),
itemBuilder: (context, index) {
BankAccount account = accountList[index];
final account = accountList[index];
return PanelListTile(
name: account.name,
color: accountColorList[account.color],
icon: accountIconList[account.symbol],
color: account.color,
icon: account.icon,
transactions: transactions[account.id] ?? [],
amount: amounts[account.id] ?? 0,
percent: (amounts[account.id] ?? 0) / total * 100,
Expand Down
29 changes: 29 additions & 0 deletions lib/providers/categories_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ class ParentCategoryWithSubcategoriesData {
Future<List<ParentCategoryWithSubcategoriesData>> categoryWithSubcategoriesData(
Ref ref,
) async {
const uncategorizedCategoryId = 0;
final trnscType = ref.watch(selectedTransactionTypeProvider);
final categories = ref.watch(categoriesProvider).value ?? [];
final parentCategories = ref.watch(allParentCategoriesProvider).value ?? [];
Expand Down Expand Up @@ -419,5 +420,33 @@ Future<List<ParentCategoryWithSubcategoriesData>> categoryWithSubcategoriesData(
}
}

final uncategorizedTransactions = transactions
.where((trnsc) => trnsc.type == trnscType && trnsc.idCategory == null)
.toList();
final uncategorizedTotal = uncategorizedTransactions.fold<num>(
0,
(previousValue, trnsc) => previousValue + trnsc.amount,
);

if (uncategorizedTotal != 0) {
result.add(
ParentCategoryWithSubcategoriesData(
parentCategory: CategoryTransaction(
id: uncategorizedCategoryId,
name: 'Uncategorized',
type: trnscType.categoryType!,
symbol: 'question_mark',
color: 0,
order: result.length,
),
subcategories: const {},
transactions: uncategorizedTransactions,
total: trnscType == TransactionType.expense
? -uncategorizedTotal
: uncategorizedTotal,
),
);
}

return result;
}
Loading
Loading