mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
@@ -11,7 +11,6 @@ class App extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeMode = ref.watch(themeProvider);
|
||||
|
||||
// Trigger exchange rate fetch on app start
|
||||
ref.watch(ratesInitProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
|
||||
@@ -211,5 +211,4 @@ class AppTheme {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep for backward compatibility
|
||||
ThemeData buildAppTheme() => AppTheme.darkTheme;
|
||||
|
||||
@@ -12,7 +12,6 @@ class AppColors {
|
||||
static const divider = Color(0xFF2A2A38);
|
||||
static const warning = Color(0xFFFFB74D);
|
||||
|
||||
// Light theme colors
|
||||
static const lightBackground = Color(0xFFF5F5F7);
|
||||
static const lightSurface = Color(0xFFFFFFFF);
|
||||
static const lightTextPrimary = Color(0xFF1A1A24);
|
||||
@@ -96,7 +95,6 @@ extension AmountFormatExt on AmountFormat {
|
||||
String format(double amount) {
|
||||
switch (this) {
|
||||
case AmountFormat.commasDot:
|
||||
// groups of 3 with commas, dot decimal
|
||||
final parts = amount.toStringAsFixed(2).split('.');
|
||||
final intPart = parts[0].replaceAllMapped(
|
||||
RegExp(r'(\d)(?=(\d{3})+$)'), (m) => '${m[1]},');
|
||||
|
||||
@@ -79,7 +79,6 @@ class AddTransactionNotifier extends StateNotifier<AddTransactionState> {
|
||||
void setCategory(String v) => state = state.copyWith(category: v);
|
||||
|
||||
void setType(TransactionType v) {
|
||||
// Reset category to first item of new type
|
||||
final newCategory = AppCategories.forType(v).first;
|
||||
state = state.copyWith(type: v, category: newCategory);
|
||||
}
|
||||
@@ -102,7 +101,6 @@ final addTransactionProvider = StateNotifierProvider.autoDispose
|
||||
(ref, initial) => AddTransactionNotifier(initial),
|
||||
);
|
||||
|
||||
// Reactive categories based on selected type
|
||||
final availableCategoriesProvider =
|
||||
Provider.autoDispose.family<List<String>, Transaction?>((ref, initial) {
|
||||
final type = ref.watch(addTransactionProvider(initial).select((s) => s.type));
|
||||
|
||||
@@ -40,7 +40,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
_amountController.text = widget.initial!.amount.toString();
|
||||
_noteController.text = widget.initial!.note ?? '';
|
||||
} else {
|
||||
// Set default currency from global provider after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final curr = ref.read(currencyProvider);
|
||||
ref.read(addTransactionProvider(null).notifier).setCurrency(curr.symbol, curr.code);
|
||||
@@ -161,7 +160,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
// Type toggle
|
||||
_TypeToggle(
|
||||
selected: state.type,
|
||||
onChanged: (t) =>
|
||||
@@ -169,7 +167,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Amount
|
||||
_SectionLabel('Amount'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
@@ -231,7 +228,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Currency
|
||||
Text(
|
||||
'Currency',
|
||||
style: TextStyle(
|
||||
@@ -247,7 +243,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Category
|
||||
_SectionLabel('Category'),
|
||||
const SizedBox(height: 8),
|
||||
_CategoryPicker(
|
||||
@@ -258,7 +253,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Date
|
||||
_SectionLabel('Date'),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
@@ -288,7 +282,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Note
|
||||
_SectionLabel('Note (optional)'),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
|
||||
@@ -13,7 +13,6 @@ final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
|
||||
return map;
|
||||
});
|
||||
|
||||
// Monthly breakdown for last 6 months
|
||||
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
||||
final txs = ref.watch(transactionsProvider)
|
||||
.where((t) => t.type == TransactionType.expense);
|
||||
|
||||
@@ -29,7 +29,6 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
||||
final total = data.values.fold(0.0, (a, b) => a + b);
|
||||
final currencyInfo = ref.watch(currencyProvider);
|
||||
|
||||
// Sort categories by amount descending
|
||||
final sortedEntries = data.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
|
||||
@@ -45,12 +45,10 @@ class TransactionsNotifier extends StateNotifier<List<Transaction>> {
|
||||
|
||||
void clearAll() {
|
||||
state = [];
|
||||
// also clear from SharedPreferences:
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove('transactions'));
|
||||
}
|
||||
}
|
||||
|
||||
// Search and filter state
|
||||
final searchQueryProvider = StateProvider<String>((ref) => '');
|
||||
|
||||
enum TransactionFilter { all, income, expense }
|
||||
@@ -58,7 +56,6 @@ enum TransactionFilter { all, income, expense }
|
||||
final transactionFilterProvider =
|
||||
StateProvider<TransactionFilter>((ref) => TransactionFilter.all);
|
||||
|
||||
// Converted balance providers (convert all transactions to selected currency)
|
||||
final totalBalanceProvider = Provider<double>((ref) {
|
||||
final txs = ref.watch(transactionsProvider);
|
||||
final exchangeService = ref.watch(exchangeRateServiceProvider);
|
||||
@@ -111,14 +108,12 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
||||
|
||||
var filtered = txs;
|
||||
|
||||
// Apply type filter
|
||||
if (filter == TransactionFilter.income) {
|
||||
filtered = filtered.where((t) => t.type == TransactionType.income).toList();
|
||||
} else if (filter == TransactionFilter.expense) {
|
||||
filtered = filtered.where((t) => t.type == TransactionType.expense).toList();
|
||||
}
|
||||
|
||||
// Apply search query
|
||||
if (query.isNotEmpty) {
|
||||
filtered = filtered.where((t) {
|
||||
final matchesCategory = t.category.toLowerCase().contains(query);
|
||||
@@ -127,7 +122,6 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Sort by date descending
|
||||
filtered.sort((a, b) => b.date.compareTo(a.date));
|
||||
return filtered;
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import '../../shared/providers/amount_format_provider.dart';
|
||||
import '../settings/provider.dart';
|
||||
import 'provider.dart';
|
||||
|
||||
// Helper for balance card only - hides .00 decimals
|
||||
String _smartBalance(double amount, AmountFormat fmt, String symbol) {
|
||||
const spaceAfter = {'Br'};
|
||||
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
||||
@@ -20,7 +19,6 @@ String _smartBalance(double amount, AmountFormat fmt, String symbol) {
|
||||
|
||||
String formatted;
|
||||
if (isWhole) {
|
||||
// format the integer, then manually remove .00
|
||||
formatted = fmt.format(amount);
|
||||
if (formatted.endsWith('.00')) {
|
||||
formatted = formatted.substring(0, formatted.length - 3);
|
||||
|
||||
@@ -32,7 +32,6 @@ class BudgetNotifier extends StateNotifier<double?> {
|
||||
}
|
||||
}
|
||||
|
||||
// Currency info: symbol and code
|
||||
class CurrencyInfo {
|
||||
final String symbol;
|
||||
final String code;
|
||||
@@ -95,7 +94,6 @@ final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||
(ref) => ThemeModeNotifier(),
|
||||
);
|
||||
|
||||
// Exchange rate service
|
||||
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
|
||||
final prefs = ref.watch(sharedPreferencesProvider);
|
||||
return ExchangeRateService(prefs);
|
||||
@@ -119,11 +117,9 @@ class ExportService {
|
||||
final currency = _ref.read(currencyProvider);
|
||||
final fmt = _ref.read(amountFormatProvider);
|
||||
|
||||
// CSV header
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('Date,Type,Category,Amount,Currency,Note');
|
||||
|
||||
// CSV rows
|
||||
for (final tx in transactions) {
|
||||
final date = DateFormat('yyyy-MM-dd').format(tx.date);
|
||||
final type = tx.type.name;
|
||||
@@ -133,7 +129,6 @@ class ExportService {
|
||||
buffer.writeln('$date,$type,$category,$amount,${tx.currencyCode},$note');
|
||||
}
|
||||
|
||||
// Save to Downloads
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
||||
final file = File('${directory.path}/transactions_$timestamp.csv');
|
||||
|
||||
@@ -52,7 +52,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
}
|
||||
|
||||
void _confirmClearData(BuildContext context, WidgetRef ref) {
|
||||
// First confirmation
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
@@ -66,7 +65,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// Second confirmation
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx2) => AlertDialog(
|
||||
@@ -109,7 +107,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
final fmt = ref.watch(amountFormatProvider);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Update currency format when it changes
|
||||
_currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2);
|
||||
|
||||
return Scaffold(
|
||||
@@ -139,7 +136,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||
children: [
|
||||
|
||||
// Theme Toggle
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -194,7 +190,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Budget Setting
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -310,7 +305,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Amount Format Selector
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -393,7 +387,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Currency Selector
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -486,7 +479,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Danger Zone
|
||||
Text(
|
||||
'Danger Zone',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -40,7 +40,6 @@ class ExchangeRateService {
|
||||
|
||||
Future<void> fetchRates() async {
|
||||
try {
|
||||
// Try primary URL
|
||||
final response = await http
|
||||
.get(Uri.parse(_primaryUrl))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -60,11 +59,9 @@ class ExchangeRateService {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Primary failed, try fallback
|
||||
}
|
||||
|
||||
try {
|
||||
// Try fallback URL
|
||||
final response = await http
|
||||
.get(Uri.parse(_fallbackUrl))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
@@ -84,10 +81,8 @@ class ExchangeRateService {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Both failed, use cached or fallback
|
||||
}
|
||||
|
||||
// If both failed and no cache, use fallback
|
||||
if (_rates.isEmpty) {
|
||||
_rates = Map.from(_fallbackRates);
|
||||
}
|
||||
@@ -104,7 +99,6 @@ class ExchangeRateService {
|
||||
final fromRate = currentRates[from] ?? 1.0;
|
||||
final toRate = currentRates[to] ?? 1.0;
|
||||
|
||||
// Convert to USD first, then to target currency
|
||||
final amountInUsd = amount / fromRate;
|
||||
return amountInUsd * toRate;
|
||||
}
|
||||
|
||||
@@ -70,14 +70,13 @@ class StorageService {
|
||||
}
|
||||
|
||||
bool loadThemeMode() {
|
||||
return _prefs.getBool(_themeKey) ?? true; // default dark
|
||||
return _prefs.getBool(_themeKey) ?? true;
|
||||
}
|
||||
|
||||
Future<void> saveThemeMode(bool isDark) async {
|
||||
await _prefs.setBool(_themeKey, isDark);
|
||||
}
|
||||
|
||||
// Process recurring transactions
|
||||
Future<void> processRecurringTransactions() async {
|
||||
final transactions = loadTransactions();
|
||||
final now = DateTime.now();
|
||||
@@ -115,7 +114,6 @@ class StorageService {
|
||||
}
|
||||
|
||||
if (shouldCreate) {
|
||||
// Create new occurrence
|
||||
final newTx = Transaction(
|
||||
id: _uuid.v4(),
|
||||
amount: tx.amount,
|
||||
@@ -128,7 +126,6 @@ class StorageService {
|
||||
);
|
||||
transactions.add(newTx);
|
||||
|
||||
// Update original transaction's lastOccurrence
|
||||
final index = transactions.indexWhere((t) => t.id == tx.id);
|
||||
if (index != -1) {
|
||||
transactions[index] = tx.copyWith(lastOccurrence: today);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import '../../core/constants.dart';
|
||||
|
||||
String formatAmount(String symbol, double amount, AmountFormat fmt) {
|
||||
// Symbols that need a space after them (prefix symbols like Br, ₽ etc.)
|
||||
const spaceAfter = {'Br'};
|
||||
final formatted = fmt.format(amount);
|
||||
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
||||
|
||||
Reference in New Issue
Block a user