delete CloudKit and added BalanceFlutter
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
Android CI / Build and Test (push) Failing after 21m18s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
This commit is contained in:
1 parent
a715eca947
commit
2c278044bb
214 files changed
+13411
-2290
No files matched your search
@@ -0,0 +1,685 @@
|
||||
/// Finance values use hundredths of a currency unit internally. JSON retains
|
||||
/// the existing iOS / Android / Go synchronization protocol.
|
||||
enum TransactionKind {
|
||||
expense,
|
||||
income;
|
||||
|
||||
String get title => this == income ? 'Доход' : 'Расход';
|
||||
static TransactionKind parse(String value) =>
|
||||
value == 'income' ? income : expense;
|
||||
}
|
||||
|
||||
int moneyToMinor(num value) {
|
||||
if (!value.isFinite) throw const FormatException('Invalid money amount');
|
||||
return (value * 100).round();
|
||||
}
|
||||
|
||||
/// Parse user-entered amounts without passing through binary floating point.
|
||||
/// An empty value, exponent, sign or more than two decimal places is rejected.
|
||||
int parseMoney(String value) {
|
||||
final normalized = value.trim().replaceAll(',', '.');
|
||||
if (!RegExp(r'^\d+(?:\.\d{1,2})?$').hasMatch(normalized)) {
|
||||
throw const FormatException(
|
||||
'Введите положительную сумму, максимум 2 знака после запятой',
|
||||
);
|
||||
}
|
||||
final parts = normalized.split('.');
|
||||
return int.parse(parts[0]) * 100 +
|
||||
(parts.length == 1 ? 0 : int.parse(parts[1].padRight(2, '0')));
|
||||
}
|
||||
|
||||
String formatMinor(int value) {
|
||||
final absolute = value.abs();
|
||||
return '${value < 0 ? '-' : ''}${absolute ~/ 100}.${(absolute % 100).toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
DateTime _date(dynamic value) => DateTime.parse(value as String);
|
||||
String _timestamp(DateTime value) => value.toUtc().toIso8601String();
|
||||
|
||||
class TransactionRecord {
|
||||
final String id;
|
||||
final int amountMinor;
|
||||
final DateTime date;
|
||||
final String note,
|
||||
categoryName,
|
||||
categoryIcon,
|
||||
categoryEmoji,
|
||||
categoryColorName;
|
||||
final bool isBalanceAdjustment;
|
||||
final String kindRawValue;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const TransactionRecord({
|
||||
required this.id,
|
||||
required this.amountMinor,
|
||||
required this.date,
|
||||
required this.categoryName,
|
||||
required this.updatedAt,
|
||||
this.note = '',
|
||||
this.categoryIcon = 'circle.fill',
|
||||
this.categoryEmoji = '',
|
||||
this.categoryColorName = 'auto',
|
||||
this.isBalanceAdjustment = false,
|
||||
this.kindRawValue = 'expense',
|
||||
});
|
||||
|
||||
double get amount => amountMinor / 100;
|
||||
TransactionKind get kind => TransactionKind.parse(kindRawValue);
|
||||
Map<String, dynamic> payload() => {
|
||||
'amount': amount,
|
||||
'date': _timestamp(date),
|
||||
'note': note,
|
||||
'categoryName': categoryName,
|
||||
'categoryIcon': categoryIcon,
|
||||
'categoryEmoji': categoryEmoji,
|
||||
'categoryColorName': categoryColorName,
|
||||
'isBalanceAdjustment': isBalanceAdjustment,
|
||||
'kindRawValue': kindRawValue,
|
||||
};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'updatedAt': _timestamp(updatedAt),
|
||||
...payload(),
|
||||
'amountMinor': amountMinor,
|
||||
};
|
||||
factory TransactionRecord.fromJson(Map<String, dynamic> json) =>
|
||||
TransactionRecord.fromPayload(
|
||||
id: json['id'] as String,
|
||||
updatedAt: _date(json['updatedAt']),
|
||||
payload: json,
|
||||
).copyWith(amountMinor: json['amountMinor'] as int?);
|
||||
factory TransactionRecord.fromPayload({
|
||||
required String id,
|
||||
required DateTime updatedAt,
|
||||
required Map<String, dynamic> payload,
|
||||
}) => TransactionRecord(
|
||||
id: id,
|
||||
updatedAt: updatedAt,
|
||||
amountMinor: moneyToMinor(payload['amount'] as num),
|
||||
date: _date(payload['date']),
|
||||
categoryName: payload['categoryName'] as String,
|
||||
note: payload['note'] as String? ?? '',
|
||||
categoryIcon: payload['categoryIcon'] as String? ?? 'circle.fill',
|
||||
categoryEmoji: payload['categoryEmoji'] as String? ?? '',
|
||||
categoryColorName: payload['categoryColorName'] as String? ?? 'auto',
|
||||
isBalanceAdjustment: payload['isBalanceAdjustment'] as bool? ?? false,
|
||||
kindRawValue: payload['kindRawValue'] as String? ?? 'expense',
|
||||
);
|
||||
TransactionRecord copyWith({
|
||||
String? id,
|
||||
int? amountMinor,
|
||||
DateTime? date,
|
||||
String? note,
|
||||
String? categoryName,
|
||||
String? categoryIcon,
|
||||
String? categoryEmoji,
|
||||
String? categoryColorName,
|
||||
bool? isBalanceAdjustment,
|
||||
String? kindRawValue,
|
||||
DateTime? updatedAt,
|
||||
}) => TransactionRecord(
|
||||
id: id ?? this.id,
|
||||
amountMinor: amountMinor ?? this.amountMinor,
|
||||
date: date ?? this.date,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
note: note ?? this.note,
|
||||
categoryName: categoryName ?? this.categoryName,
|
||||
categoryIcon: categoryIcon ?? this.categoryIcon,
|
||||
categoryEmoji: categoryEmoji ?? this.categoryEmoji,
|
||||
categoryColorName: categoryColorName ?? this.categoryColorName,
|
||||
isBalanceAdjustment: isBalanceAdjustment ?? this.isBalanceAdjustment,
|
||||
kindRawValue: kindRawValue ?? this.kindRawValue,
|
||||
);
|
||||
}
|
||||
|
||||
class CategoryRecord {
|
||||
final String id, name, icon, emoji, colorName, kindRawValue;
|
||||
final DateTime createdAt, updatedAt;
|
||||
const CategoryRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.colorName,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.emoji = '',
|
||||
this.kindRawValue = 'expense',
|
||||
});
|
||||
TransactionKind get kind => TransactionKind.parse(kindRawValue);
|
||||
Map<String, dynamic> payload() => {
|
||||
'name': name,
|
||||
'icon': icon,
|
||||
'emoji': emoji,
|
||||
'colorName': colorName,
|
||||
'kindRawValue': kindRawValue,
|
||||
'createdAt': _timestamp(createdAt),
|
||||
};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'updatedAt': _timestamp(updatedAt),
|
||||
...payload(),
|
||||
};
|
||||
factory CategoryRecord.fromJson(Map<String, dynamic> json) =>
|
||||
CategoryRecord.fromPayload(
|
||||
id: json['id'] as String,
|
||||
updatedAt: _date(json['updatedAt']),
|
||||
payload: json,
|
||||
);
|
||||
factory CategoryRecord.fromPayload({
|
||||
required String id,
|
||||
required DateTime updatedAt,
|
||||
required Map<String, dynamic> payload,
|
||||
}) => CategoryRecord(
|
||||
id: id,
|
||||
updatedAt: updatedAt,
|
||||
name: payload['name'] as String,
|
||||
icon: payload['icon'] as String? ?? 'star.fill',
|
||||
emoji: payload['emoji'] as String? ?? '',
|
||||
colorName: payload['colorName'] as String? ?? 'indigo',
|
||||
kindRawValue: payload['kindRawValue'] as String? ?? 'expense',
|
||||
createdAt: payload['createdAt'] == null
|
||||
? updatedAt
|
||||
: _date(payload['createdAt']),
|
||||
);
|
||||
CategoryRecord copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? icon,
|
||||
String? emoji,
|
||||
String? colorName,
|
||||
String? kindRawValue,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) => CategoryRecord(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
icon: icon ?? this.icon,
|
||||
emoji: emoji ?? this.emoji,
|
||||
colorName: colorName ?? this.colorName,
|
||||
kindRawValue: kindRawValue ?? this.kindRawValue,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
class BudgetRecord {
|
||||
final String id, categoryName, categoryIcon, categoryEmoji;
|
||||
final int limitMinor;
|
||||
final DateTime monthStart, updatedAt;
|
||||
const BudgetRecord({
|
||||
required this.id,
|
||||
required this.categoryName,
|
||||
required this.limitMinor,
|
||||
required this.monthStart,
|
||||
required this.updatedAt,
|
||||
this.categoryIcon = 'circle.fill',
|
||||
this.categoryEmoji = '',
|
||||
});
|
||||
double get limit => limitMinor / 100;
|
||||
Map<String, dynamic> payload() => {
|
||||
'categoryName': categoryName,
|
||||
'categoryIcon': categoryIcon,
|
||||
'categoryEmoji': categoryEmoji,
|
||||
'limit': limit,
|
||||
'monthStart': _timestamp(monthStart),
|
||||
};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'updatedAt': _timestamp(updatedAt),
|
||||
...payload(),
|
||||
'limitMinor': limitMinor,
|
||||
};
|
||||
factory BudgetRecord.fromJson(Map<String, dynamic> json) =>
|
||||
BudgetRecord.fromPayload(
|
||||
id: json['id'] as String,
|
||||
updatedAt: _date(json['updatedAt']),
|
||||
payload: json,
|
||||
).copyWith(limitMinor: json['limitMinor'] as int?);
|
||||
factory BudgetRecord.fromPayload({
|
||||
required String id,
|
||||
required DateTime updatedAt,
|
||||
required Map<String, dynamic> payload,
|
||||
}) => BudgetRecord(
|
||||
id: id,
|
||||
updatedAt: updatedAt,
|
||||
categoryName: payload['categoryName'] as String,
|
||||
categoryIcon: payload['categoryIcon'] as String? ?? 'circle.fill',
|
||||
categoryEmoji: payload['categoryEmoji'] as String? ?? '',
|
||||
limitMinor: moneyToMinor(payload['limit'] as num),
|
||||
monthStart: _date(payload['monthStart']),
|
||||
);
|
||||
BudgetRecord copyWith({
|
||||
String? id,
|
||||
String? categoryName,
|
||||
String? categoryIcon,
|
||||
String? categoryEmoji,
|
||||
int? limitMinor,
|
||||
DateTime? monthStart,
|
||||
DateTime? updatedAt,
|
||||
}) => BudgetRecord(
|
||||
id: id ?? this.id,
|
||||
categoryName: categoryName ?? this.categoryName,
|
||||
categoryIcon: categoryIcon ?? this.categoryIcon,
|
||||
categoryEmoji: categoryEmoji ?? this.categoryEmoji,
|
||||
limitMinor: limitMinor ?? this.limitMinor,
|
||||
monthStart: monthStart ?? this.monthStart,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
class Tombstone {
|
||||
final String entity, id;
|
||||
final DateTime updatedAt;
|
||||
const Tombstone({
|
||||
required this.entity,
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
});
|
||||
String get recordID => id;
|
||||
Map<String, dynamic> toJson() => {
|
||||
'entity': entity,
|
||||
'id': id,
|
||||
'deleted': true,
|
||||
'updatedAt': _timestamp(updatedAt),
|
||||
};
|
||||
factory Tombstone.fromJson(Map<String, dynamic> json) => Tombstone(
|
||||
entity: json['entity'] as String,
|
||||
id: json['id'] as String,
|
||||
updatedAt: _date(json['updatedAt']),
|
||||
);
|
||||
}
|
||||
|
||||
class FinanceSummary {
|
||||
final int incomeMinor, expensesMinor;
|
||||
const FinanceSummary({
|
||||
required this.incomeMinor,
|
||||
required this.expensesMinor,
|
||||
});
|
||||
int get balanceMinor => incomeMinor - expensesMinor;
|
||||
double get income => incomeMinor / 100;
|
||||
double get expenses => expensesMinor / 100;
|
||||
double get balance => balanceMinor / 100;
|
||||
}
|
||||
|
||||
class CategorySpending {
|
||||
final String name, icon, emoji, colorName;
|
||||
final int amountMinor;
|
||||
const CategorySpending({
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.emoji,
|
||||
required this.colorName,
|
||||
required this.amountMinor,
|
||||
});
|
||||
double get amount => amountMinor / 100;
|
||||
}
|
||||
|
||||
class MonthlyTotal {
|
||||
final DateTime month;
|
||||
final FinanceSummary summary;
|
||||
const MonthlyTotal(this.month, this.summary);
|
||||
}
|
||||
|
||||
class FinanceMath {
|
||||
static int totalBalance(Iterable<TransactionRecord> items) => items.fold(
|
||||
0,
|
||||
(value, item) =>
|
||||
value +
|
||||
(item.kind == TransactionKind.income
|
||||
? item.amountMinor
|
||||
: -item.amountMinor),
|
||||
);
|
||||
static DateTime monthStart(DateTime date) => date.isUtc
|
||||
? DateTime.utc(date.year, date.month)
|
||||
: DateTime(date.year, date.month);
|
||||
static DateTime nextMonth(DateTime date) => date.isUtc
|
||||
? DateTime.utc(date.year, date.month + 1)
|
||||
: DateTime(date.year, date.month + 1);
|
||||
static bool _inPeriod(TransactionRecord item, DateTime start, DateTime end) =>
|
||||
!item.isBalanceAdjustment &&
|
||||
!item.date.isBefore(start) &&
|
||||
item.date.isBefore(end);
|
||||
static FinanceSummary summary(
|
||||
Iterable<TransactionRecord> items,
|
||||
DateTime start,
|
||||
DateTime endExclusive,
|
||||
) {
|
||||
var income = 0;
|
||||
var expenses = 0;
|
||||
for (final item in items) {
|
||||
if (!_inPeriod(item, start, endExclusive)) continue;
|
||||
if (item.kind == TransactionKind.income) {
|
||||
income += item.amountMinor;
|
||||
} else {
|
||||
expenses += item.amountMinor;
|
||||
}
|
||||
}
|
||||
return FinanceSummary(incomeMinor: income, expensesMinor: expenses);
|
||||
}
|
||||
|
||||
static List<CategorySpending> spending(
|
||||
Iterable<TransactionRecord> items,
|
||||
DateTime start,
|
||||
DateTime endExclusive,
|
||||
) {
|
||||
final grouped = <String, CategorySpending>{};
|
||||
for (final item in items) {
|
||||
if (item.kind != TransactionKind.expense ||
|
||||
!_inPeriod(item, start, endExclusive)) {
|
||||
continue;
|
||||
}
|
||||
final existing = grouped[item.categoryName];
|
||||
grouped[item.categoryName] = CategorySpending(
|
||||
name: item.categoryName,
|
||||
icon: existing?.icon ?? item.categoryIcon,
|
||||
emoji: existing?.emoji ?? item.categoryEmoji,
|
||||
colorName: existing?.colorName ?? item.categoryColorName,
|
||||
amountMinor: (existing?.amountMinor ?? 0) + item.amountMinor,
|
||||
);
|
||||
}
|
||||
return grouped.values.toList()..sort((a, b) {
|
||||
final byAmount = b.amountMinor.compareTo(a.amountMinor);
|
||||
return byAmount == 0 ? a.name.compareTo(b.name) : byAmount;
|
||||
});
|
||||
}
|
||||
|
||||
/// Includes empty months, ordered from oldest to newest.
|
||||
static List<MonthlyTotal> monthlyTrend(
|
||||
Iterable<TransactionRecord> items,
|
||||
DateTime through, {
|
||||
int months = 6,
|
||||
}) {
|
||||
if (months < 0) throw ArgumentError.value(months, 'months');
|
||||
final snapshot = items.toList();
|
||||
return List.generate(months, (index) {
|
||||
final month = through.isUtc
|
||||
? DateTime.utc(through.year, through.month - months + 1 + index)
|
||||
: DateTime(through.year, through.month - months + 1 + index);
|
||||
return MonthlyTotal(month, summary(snapshot, month, nextMonth(month)));
|
||||
});
|
||||
}
|
||||
|
||||
static int budgetUsed(
|
||||
BudgetRecord budget,
|
||||
Iterable<TransactionRecord> items,
|
||||
) {
|
||||
// Native clients serialize local month starts as UTC instants. In positive
|
||||
// offsets January 1 becomes December 31 on the wire; recover the local
|
||||
// calendar month before building its spending interval.
|
||||
final start = monthStart(budget.monthStart.toLocal());
|
||||
return items
|
||||
.where(
|
||||
(item) =>
|
||||
item.categoryName == budget.categoryName &&
|
||||
item.kind == TransactionKind.expense &&
|
||||
_inPeriod(item, start, nextMonth(start)),
|
||||
)
|
||||
.fold(0, (value, item) => value + item.amountMinor);
|
||||
}
|
||||
|
||||
static String escapeCsv(String value) => '"${value.replaceAll('"', '""')}"';
|
||||
static String exportCsv(Iterable<TransactionRecord> items) {
|
||||
final ordered = items.toList()..sort((a, b) => b.date.compareTo(a.date));
|
||||
return [
|
||||
'Дата,Тип,Категория,Сумма,Заметка',
|
||||
...ordered.map(
|
||||
(item) => [
|
||||
_timestamp(item.date),
|
||||
item.isBalanceAdjustment ? 'Корректировка' : item.kind.title,
|
||||
item.categoryName,
|
||||
formatMinor(item.amountMinor),
|
||||
item.note,
|
||||
].map(escapeCsv).join(','),
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDesignOptions {
|
||||
static const icons = <String>[
|
||||
'star.fill',
|
||||
'heart.fill',
|
||||
'sparkles',
|
||||
'bolt.fill',
|
||||
'flame.fill',
|
||||
'drop.fill',
|
||||
'cart.fill',
|
||||
'bag.fill',
|
||||
'creditcard.fill',
|
||||
'banknote.fill',
|
||||
'gift.fill',
|
||||
'tag.fill',
|
||||
'cup.and.saucer.fill',
|
||||
'fork.knife',
|
||||
'takeoutbag.and.cup.and.straw.fill',
|
||||
'birthday.cake.fill',
|
||||
'car.fill',
|
||||
'bus.fill',
|
||||
'tram.fill',
|
||||
'bicycle',
|
||||
'airplane',
|
||||
'fuelpump.fill',
|
||||
'house.fill',
|
||||
'bed.double.fill',
|
||||
'lightbulb.fill',
|
||||
'wifi',
|
||||
'phone.fill',
|
||||
'printer.fill',
|
||||
'pawprint.fill',
|
||||
'leaf.fill',
|
||||
'figure.walk',
|
||||
'figure.run',
|
||||
'dumbbell.fill',
|
||||
'cross.case.fill',
|
||||
'pills.fill',
|
||||
'stethoscope',
|
||||
'book.fill',
|
||||
'graduationcap.fill',
|
||||
'laptopcomputer',
|
||||
'briefcase.fill',
|
||||
'gamecontroller.fill',
|
||||
'music.note',
|
||||
'film.fill',
|
||||
'camera.fill',
|
||||
'paintpalette.fill',
|
||||
'tshirt.fill',
|
||||
'hammer.fill',
|
||||
'wrench.and.screwdriver.fill',
|
||||
'shippingbox.fill',
|
||||
'globe.europe.africa.fill',
|
||||
];
|
||||
static const emojis = <String>[
|
||||
'🍔',
|
||||
'☕️',
|
||||
'🛒',
|
||||
'🚕',
|
||||
'✈️',
|
||||
'🏠',
|
||||
'💡',
|
||||
'📱',
|
||||
'💻',
|
||||
'🎮',
|
||||
'🎬',
|
||||
'🎵',
|
||||
'📚',
|
||||
'🎓',
|
||||
'💊',
|
||||
'🏋️',
|
||||
'🐶',
|
||||
'🐱',
|
||||
'👕',
|
||||
'🎁',
|
||||
'💰',
|
||||
'💳',
|
||||
'📈',
|
||||
'🔧',
|
||||
'🌿',
|
||||
'❤️',
|
||||
'⭐️',
|
||||
'🎯',
|
||||
'🧾',
|
||||
'🏖️',
|
||||
];
|
||||
static const colors = <String>[
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'mint',
|
||||
'teal',
|
||||
'cyan',
|
||||
'blue',
|
||||
'indigo',
|
||||
'purple',
|
||||
'pink',
|
||||
'gray',
|
||||
'coral',
|
||||
'peach',
|
||||
'gold',
|
||||
'lime',
|
||||
'forest',
|
||||
'turquoise',
|
||||
'sky',
|
||||
'navy',
|
||||
'lavender',
|
||||
'magenta',
|
||||
'brown',
|
||||
'slate',
|
||||
];
|
||||
}
|
||||
|
||||
class BuiltInCategories {
|
||||
static final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
static final expense = List<CategoryRecord>.unmodifiable([
|
||||
CategoryRecord(
|
||||
id: 'food',
|
||||
name: 'Продукты',
|
||||
icon: 'cart.fill',
|
||||
colorName: 'orange',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'transport',
|
||||
name: 'Транспорт',
|
||||
icon: 'car.fill',
|
||||
colorName: 'blue',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'home',
|
||||
name: 'Дом',
|
||||
icon: 'house.fill',
|
||||
colorName: 'indigo',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'entertainment',
|
||||
name: 'Развлечения',
|
||||
icon: 'gamecontroller.fill',
|
||||
colorName: 'pink',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'health',
|
||||
name: 'Здоровье',
|
||||
icon: 'cross.case.fill',
|
||||
colorName: 'red',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'shopping',
|
||||
name: 'Покупки',
|
||||
icon: 'bag.fill',
|
||||
colorName: 'purple',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'subscriptions',
|
||||
name: 'Подписки',
|
||||
icon: 'repeat',
|
||||
colorName: 'cyan',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'education',
|
||||
name: 'Образование',
|
||||
icon: 'book.fill',
|
||||
colorName: 'mint',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'other-expense',
|
||||
name: 'Другое',
|
||||
icon: 'ellipsis.circle.fill',
|
||||
colorName: 'gray',
|
||||
kindRawValue: 'expense',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
]);
|
||||
static final income = List<CategoryRecord>.unmodifiable([
|
||||
CategoryRecord(
|
||||
id: 'salary',
|
||||
name: 'Зарплата',
|
||||
icon: 'banknote.fill',
|
||||
colorName: 'green',
|
||||
kindRawValue: 'income',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'freelance',
|
||||
name: 'Подработка',
|
||||
icon: 'laptopcomputer',
|
||||
colorName: 'teal',
|
||||
kindRawValue: 'income',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'investments',
|
||||
name: 'Инвестиции',
|
||||
icon: 'chart.line.uptrend.xyaxis',
|
||||
colorName: 'blue',
|
||||
kindRawValue: 'income',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'gift',
|
||||
name: 'Подарок',
|
||||
icon: 'gift.fill',
|
||||
colorName: 'pink',
|
||||
kindRawValue: 'income',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
CategoryRecord(
|
||||
id: 'other-income',
|
||||
name: 'Другое',
|
||||
icon: 'plus.circle.fill',
|
||||
colorName: 'gray',
|
||||
kindRawValue: 'income',
|
||||
createdAt: _epoch,
|
||||
updatedAt: _epoch,
|
||||
),
|
||||
]);
|
||||
static List<CategoryRecord> forKind(String kind) =>
|
||||
kind == 'income' ? income : expense;
|
||||
}
|
||||
Reference in new issue
Block a user