mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
stableee
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
import '../models/transaction.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
@@ -15,50 +16,102 @@ class StorageService {
|
||||
|
||||
StorageService(this._prefs);
|
||||
|
||||
List<Transaction> loadTransactions() {
|
||||
final raw = _prefs.getString(_transactionsKey);
|
||||
if (raw == null) return [];
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
return list
|
||||
.map((e) => Transaction.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
/// Load all transactions with error handling
|
||||
Result<List<Transaction>> loadTransactions() {
|
||||
return resultOf(() {
|
||||
final raw = _prefs.getString(_transactionsKey);
|
||||
if (raw == null) return <Transaction>[];
|
||||
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
return list
|
||||
.map((e) => Transaction.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> saveTransactions(List<Transaction> transactions) async {
|
||||
final encoded = jsonEncode(transactions.map((t) => t.toJson()).toList());
|
||||
await _prefs.setString(_transactionsKey, encoded);
|
||||
/// Load transactions (legacy - throws on error)
|
||||
List<Transaction> loadTransactionsUnsafe() {
|
||||
final result = loadTransactions();
|
||||
return result.getOrDefault([]);
|
||||
}
|
||||
|
||||
Future<void> addTransaction(Transaction transaction) async {
|
||||
final list = loadTransactions();
|
||||
list.add(transaction);
|
||||
await saveTransactions(list);
|
||||
/// Save transactions with error handling
|
||||
Future<Result<void>> saveTransactions(List<Transaction> transactions) async {
|
||||
return asyncResultOf(() async {
|
||||
final encoded = jsonEncode(transactions.map((t) => t.toJson()).toList());
|
||||
await _prefs.setString(_transactionsKey, encoded);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTransaction(Transaction transaction) async {
|
||||
final list = loadTransactions();
|
||||
final index = list.indexWhere((t) => t.id == transaction.id);
|
||||
if (index != -1) {
|
||||
/// Add transaction with error handling
|
||||
Future<Result<void>> addTransaction(Transaction transaction) async {
|
||||
return asyncResultOf(() async {
|
||||
final listResult = loadTransactions();
|
||||
final list = listResult.getOrDefault([]);
|
||||
list.add(transaction);
|
||||
|
||||
final saveResult = await saveTransactions(list);
|
||||
if (saveResult.isFailure) {
|
||||
throw Exception(saveResult.errorOrNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update transaction with error handling
|
||||
Future<Result<void>> updateTransaction(Transaction transaction) async {
|
||||
return asyncResultOf(() async {
|
||||
final listResult = loadTransactions();
|
||||
final list = listResult.getOrDefault([]);
|
||||
|
||||
final index = list.indexWhere((t) => t.id == transaction.id);
|
||||
if (index == -1) {
|
||||
throw Exception('Transaction not found: ${transaction.id}');
|
||||
}
|
||||
|
||||
list[index] = transaction;
|
||||
await saveTransactions(list);
|
||||
}
|
||||
final saveResult = await saveTransactions(list);
|
||||
if (saveResult.isFailure) {
|
||||
throw Exception(saveResult.errorOrNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteTransaction(String id) async {
|
||||
final list = loadTransactions()..removeWhere((t) => t.id == id);
|
||||
await saveTransactions(list);
|
||||
/// Delete transaction with error handling
|
||||
Future<Result<void>> deleteTransaction(String id) async {
|
||||
return asyncResultOf(() async {
|
||||
final listResult = loadTransactions();
|
||||
final list = listResult.getOrDefault([]);
|
||||
|
||||
final initialLength = list.length;
|
||||
list.removeWhere((t) => t.id == id);
|
||||
|
||||
if (list.length == initialLength) {
|
||||
throw Exception('Transaction not found: $id');
|
||||
}
|
||||
|
||||
final saveResult = await saveTransactions(list);
|
||||
if (saveResult.isFailure) {
|
||||
throw Exception(saveResult.errorOrNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
double? loadBudget() {
|
||||
return _prefs.getDouble(_budgetKey);
|
||||
}
|
||||
|
||||
Future<void> saveBudget(double? budget) async {
|
||||
if (budget == null) {
|
||||
await _prefs.remove(_budgetKey);
|
||||
} else {
|
||||
await _prefs.setDouble(_budgetKey, budget);
|
||||
}
|
||||
/// Save budget with error handling
|
||||
Future<Result<void>> saveBudget(double? budget) async {
|
||||
return asyncResultOf(() async {
|
||||
if (budget == null) {
|
||||
await _prefs.remove(_budgetKey);
|
||||
} else {
|
||||
if (budget < 0) {
|
||||
throw Exception('Budget cannot be negative');
|
||||
}
|
||||
await _prefs.setDouble(_budgetKey, budget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String loadCurrency() {
|
||||
@@ -78,7 +131,8 @@ class StorageService {
|
||||
}
|
||||
|
||||
Future<void> processRecurringTransactions() async {
|
||||
final transactions = loadTransactions();
|
||||
final transactionsResult = loadTransactions();
|
||||
final transactions = transactionsResult.getOrDefault([]);
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
bool hasChanges = false;
|
||||
@@ -104,7 +158,8 @@ class StorageService {
|
||||
shouldCreate = daysDiff >= 7;
|
||||
break;
|
||||
case RecurrenceType.monthly:
|
||||
shouldCreate = (today.year > lastDate.year ||
|
||||
shouldCreate =
|
||||
(today.year > lastDate.year ||
|
||||
(today.year == lastDate.year &&
|
||||
today.month > lastDate.month)) &&
|
||||
today.day >= lastDate.day;
|
||||
@@ -139,4 +194,80 @@ class StorageService {
|
||||
await saveTransactions(transactions);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process recurring transactions with error handling (returns count)
|
||||
Future<Result<int>> processRecurringTransactionsWithResult() async {
|
||||
return asyncResultOf(() async {
|
||||
final transactionsResult = loadTransactions();
|
||||
final transactions = transactionsResult.getOrDefault([]);
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
int createdCount = 0;
|
||||
|
||||
for (final tx in transactions) {
|
||||
if (tx.recurrence == RecurrenceType.none) continue;
|
||||
|
||||
final lastOccurrence = tx.lastOccurrence ?? tx.date;
|
||||
final lastDate = DateTime(
|
||||
lastOccurrence.year,
|
||||
lastOccurrence.month,
|
||||
lastOccurrence.day,
|
||||
);
|
||||
|
||||
bool shouldCreate = false;
|
||||
|
||||
switch (tx.recurrence) {
|
||||
case RecurrenceType.daily:
|
||||
shouldCreate = today.isAfter(lastDate);
|
||||
break;
|
||||
case RecurrenceType.weekly:
|
||||
final daysDiff = today.difference(lastDate).inDays;
|
||||
shouldCreate = daysDiff >= 7;
|
||||
break;
|
||||
case RecurrenceType.monthly:
|
||||
shouldCreate =
|
||||
(today.year > lastDate.year ||
|
||||
(today.year == lastDate.year &&
|
||||
today.month > lastDate.month)) &&
|
||||
today.day >= lastDate.day;
|
||||
break;
|
||||
case RecurrenceType.none:
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldCreate) {
|
||||
final newTx = Transaction(
|
||||
id: _uuid.v4(),
|
||||
amount: tx.amount,
|
||||
category: tx.category,
|
||||
type: tx.type,
|
||||
date: today,
|
||||
note: tx.note,
|
||||
recurrence: tx.recurrence,
|
||||
lastOccurrence: today,
|
||||
currency: tx.currency,
|
||||
currencyCode: tx.currencyCode,
|
||||
);
|
||||
transactions.add(newTx);
|
||||
|
||||
final index = transactions.indexWhere((t) => t.id == tx.id);
|
||||
if (index != -1) {
|
||||
transactions[index] = tx.copyWith(lastOccurrence: today);
|
||||
}
|
||||
|
||||
createdCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (createdCount > 0) {
|
||||
final saveResult = await saveTransactions(transactions);
|
||||
if (saveResult.isFailure) {
|
||||
throw Exception('Failed to save recurring transactions');
|
||||
}
|
||||
}
|
||||
|
||||
return createdCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
|
||||
/// Show error snackbar with custom styling
|
||||
void showErrorSnackbar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(message, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFFE05C6B),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show success snackbar with custom styling
|
||||
void showSuccessSnackbar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(message, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF4CAF8C),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show warning snackbar with custom styling
|
||||
void showWarningSnackbar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(message, style: const TextStyle(fontSize: 14))),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFFFFB74D),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Extension to handle Result with UI feedback
|
||||
extension ResultUIExtension<T> on Result<T> {
|
||||
/// Show snackbar on failure
|
||||
Result<T> showErrorOnFailure(BuildContext context) {
|
||||
onFailure((message) => showErrorSnackbar(context, message));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Show snackbar on success with custom message
|
||||
Result<T> showSuccessMessage(BuildContext context, String message) {
|
||||
onSuccess((_) => showSuccessSnackbar(context, message));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension for Future<Result<T>>
|
||||
extension FutureResultUIExtension<T> on Future<Result<T>> {
|
||||
/// Show snackbar on failure
|
||||
Future<Result<T>> showErrorOnFailure(BuildContext context) async {
|
||||
final result = await this;
|
||||
result.onFailure((message) => showErrorSnackbar(context, message));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Show snackbar on success with custom message
|
||||
Future<Result<T>> showSuccessMessage(
|
||||
BuildContext context,
|
||||
String message,
|
||||
) async {
|
||||
final result = await this;
|
||||
result.onSuccess((_) => showSuccessSnackbar(context, message));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Show both success and error messages
|
||||
Future<Result<T>> showFeedback(
|
||||
BuildContext context, {
|
||||
required String successMessage,
|
||||
}) async {
|
||||
final result = await this;
|
||||
result
|
||||
.onSuccess((_) => showSuccessSnackbar(context, successMessage))
|
||||
.onFailure((message) => showErrorSnackbar(context, message));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Error dialog widget
|
||||
class ErrorDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const ErrorDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Color(0xFFE05C6B), size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
if (onRetry != null)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onRetry!();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
VoidCallback? onRetry,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
ErrorDialog(title: title, message: message, onRetry: onRetry),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user