Update UI components with modern iOS design and improve transaction management UI
This commit is contained in:
parent
78c47819d0
commit
ce09f987c9
6 changed files with 604 additions and 146 deletions
110
Coinly/Core/Style/AppStyle.swift
Normal file
110
Coinly/Core/Style/AppStyle.swift
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// AppStyle.swift
|
||||
// Coinly
|
||||
//
|
||||
// Created by Vadym Samoilenko on 02/03/2025.
|
||||
//
|
||||
|
||||
|
||||
// AppStyle.swift
|
||||
import SwiftUI
|
||||
|
||||
enum AppStyle {
|
||||
// Colors
|
||||
static let backgroundPrimary = Color(uiColor: .systemBackground)
|
||||
static let backgroundSecondary = Color(uiColor: .secondarySystemBackground)
|
||||
static let backgroundTertiary = Color(uiColor: .tertiarySystemBackground)
|
||||
|
||||
static let labelPrimary = Color(uiColor: .label)
|
||||
static let labelSecondary = Color(uiColor: .secondaryLabel)
|
||||
static let labelTertiary = Color(uiColor: .tertiaryLabel)
|
||||
|
||||
// Corner Radius
|
||||
static let cornerRadiusSmall: CGFloat = 8
|
||||
static let cornerRadiusMedium: CGFloat = 12
|
||||
static let cornerRadiusLarge: CGFloat = 16
|
||||
|
||||
// Padding
|
||||
static let paddingSmall: CGFloat = 8
|
||||
static let paddingMedium: CGFloat = 16
|
||||
static let paddingLarge: CGFloat = 24
|
||||
|
||||
// Font Sizes
|
||||
static let fontTitle = Font.system(size: 34, weight: .bold)
|
||||
static let fontTitle2 = Font.system(size: 28, weight: .semibold)
|
||||
static let fontTitle3 = Font.system(size: 22, weight: .semibold)
|
||||
static let fontHeadline = Font.system(size: 17, weight: .semibold)
|
||||
static let fontBody = Font.system(size: 17, weight: .regular)
|
||||
static let fontCallout = Font.system(size: 16, weight: .regular)
|
||||
static let fontSubheadline = Font.system(size: 15, weight: .regular)
|
||||
static let fontFootnote = Font.system(size: 13, weight: .regular)
|
||||
static let fontCaption = Font.system(size: 12, weight: .regular)
|
||||
|
||||
// Shadows
|
||||
struct ShadowModifier: ViewModifier {
|
||||
let radius: CGFloat
|
||||
let y: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.shadow(
|
||||
color: Color.black.opacity(0.1),
|
||||
radius: radius,
|
||||
x: 0,
|
||||
y: y
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom ViewModifiers
|
||||
struct CardStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background(backgroundSecondary)
|
||||
.cornerRadius(cornerRadiusLarge)
|
||||
.modifier(ShadowModifier(radius: 10, y: 2))
|
||||
}
|
||||
}
|
||||
|
||||
struct PrimaryButtonStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.font(fontHeadline)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, paddingMedium)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(cornerRadiusMedium)
|
||||
}
|
||||
}
|
||||
|
||||
struct SecondaryButtonStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.font(fontHeadline)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.vertical, paddingMedium)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(backgroundSecondary)
|
||||
.cornerRadius(cornerRadiusMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension для удобного использования стилей
|
||||
extension View {
|
||||
func cardStyle() -> some View {
|
||||
modifier(AppStyle.CardStyle())
|
||||
}
|
||||
|
||||
func primaryButtonStyle() -> some View {
|
||||
modifier(AppStyle.PrimaryButtonStyle())
|
||||
}
|
||||
|
||||
func secondaryButtonStyle() -> some View {
|
||||
modifier(AppStyle.SecondaryButtonStyle())
|
||||
}
|
||||
|
||||
func appShadow(radius: CGFloat = 10, y: CGFloat = 2) -> some View {
|
||||
modifier(AppStyle.ShadowModifier(radius: radius, y: y))
|
||||
}
|
||||
}
|
||||
|
|
@ -3,49 +3,108 @@ import SwiftUI
|
|||
struct AddTransactionView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
@State private var amount: String = ""
|
||||
@State private var note: String = ""
|
||||
@State private var category: String = CategoryModel.categories[0].name
|
||||
|
||||
@State private var amount = ""
|
||||
@State private var note = ""
|
||||
@State private var category = CategoryModel.categories[0].name
|
||||
@State private var date = Date()
|
||||
@State private var type: TransactionType = .expense
|
||||
@State private var showingCategories = false
|
||||
|
||||
let addTransaction: (TransactionModel) -> Void
|
||||
|
||||
private var isValidAmount: Bool {
|
||||
guard let amountDouble = Double(amount) else { return false }
|
||||
return amountDouble > 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section("Amount") {
|
||||
HStack {
|
||||
Text(settings.currency.symbol)
|
||||
.foregroundColor(.gray)
|
||||
TextField("Amount", text: $amount)
|
||||
.keyboardType(.decimalPad)
|
||||
}
|
||||
}
|
||||
ZStack {
|
||||
Color(uiColor: .systemGroupedBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
Section("Type") {
|
||||
Picker("Type", selection: $type) {
|
||||
Text("Expense").tag(TransactionType.expense)
|
||||
Text("Income").tag(TransactionType.income)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
Section("Details") {
|
||||
Picker("Category", selection: $category) {
|
||||
ForEach(CategoryModel.categories) { category in
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Amount Input
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: category.icon)
|
||||
.foregroundColor(Color(category.color))
|
||||
Text(category.name)
|
||||
Spacer()
|
||||
TextField("0", text: $amount)
|
||||
.font(.system(size: 48, weight: .medium, design: .rounded))
|
||||
.foregroundColor(type == .expense ? .red : .green)
|
||||
.multilineTextAlignment(.center)
|
||||
.keyboardType(.decimalPad)
|
||||
Spacer()
|
||||
}
|
||||
.tag(category.name)
|
||||
|
||||
Text(settings.currency.rawValue)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
// Transaction Type
|
||||
VStack {
|
||||
Picker("Type", selection: $type) {
|
||||
Text("Expense").tag(TransactionType.expense)
|
||||
Text("Income").tag(TransactionType.income)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Details
|
||||
VStack(spacing: 0) {
|
||||
Button(action: {
|
||||
showingCategories = true
|
||||
}) {
|
||||
HStack {
|
||||
let selectedCategory = CategoryModel.category(for: category)
|
||||
Image(systemName: selectedCategory.icon)
|
||||
.foregroundColor(Color(selectedCategory.color))
|
||||
Text("Category")
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
Spacer()
|
||||
Text(category)
|
||||
.foregroundColor(AppStyle.labelSecondary)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Color(uiColor: .systemGray3))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
.foregroundColor(.blue)
|
||||
DatePicker(
|
||||
"Date",
|
||||
selection: $date,
|
||||
displayedComponents: [.date, .hourAndMinute]
|
||||
)
|
||||
.labelsHidden()
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
.padding(.leading)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "text.alignleft")
|
||||
.foregroundColor(.purple)
|
||||
TextField("Note", text: $note)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
DatePicker("Date", selection: $date, displayedComponents: .date)
|
||||
|
||||
TextField("Note", text: $note)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
.navigationTitle("New Transaction")
|
||||
|
|
@ -59,20 +118,66 @@ struct AddTransactionView: View {
|
|||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Add") {
|
||||
if let amountDouble = Double(amount) {
|
||||
let transaction = TransactionModel(
|
||||
amount: amountDouble,
|
||||
date: date,
|
||||
type: type,
|
||||
category: category,
|
||||
note: note.isEmpty ? nil : note,
|
||||
originalCurrency: settings.currency // Используем текущую валюту из настроек
|
||||
)
|
||||
addTransaction(transaction)
|
||||
dismiss()
|
||||
guard let amountDouble = Double(amount) else { return }
|
||||
let transaction = TransactionModel(
|
||||
amount: amountDouble,
|
||||
date: date,
|
||||
type: type,
|
||||
category: category,
|
||||
note: note.isEmpty ? nil : note,
|
||||
originalCurrency: settings.currency
|
||||
)
|
||||
addTransaction(transaction)
|
||||
dismiss()
|
||||
}
|
||||
.disabled(!isValidAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingCategories) {
|
||||
CategoryPickerView(selectedCategory: $category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательное view для выбора категории
|
||||
struct CategoryPickerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var selectedCategory: String
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(CategoryModel.categories) { category in
|
||||
Button {
|
||||
selectedCategory = category.name
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: category.icon)
|
||||
.foregroundColor(Color(category.color))
|
||||
.frame(width: 30)
|
||||
|
||||
Text(category.name)
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
if selectedCategory == category.name {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(amount.isEmpty)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Select Category")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +186,13 @@ struct AddTransactionView: View {
|
|||
|
||||
struct AddTransactionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddTransactionView(addTransaction: { _ in })
|
||||
.environmentObject(AppSettings.shared)
|
||||
Group {
|
||||
AddTransactionView(addTransaction: { _ in })
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
AddTransactionView(addTransaction: { _ in })
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,34 +3,81 @@ import SwiftUI
|
|||
struct TransactionFilterView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var filter: TransactionFilter
|
||||
@State private var selectedPeriodIndex = 0
|
||||
|
||||
private let periods: [(name: String, period: TransactionFilter.Period)] = [
|
||||
("All Time", .all),
|
||||
("Today", .today),
|
||||
("Last 7 Days", .week),
|
||||
("Last 30 Days", .month)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
// Period Filter
|
||||
Section("Time Period") {
|
||||
Picker("Period", selection: $filter.period) {
|
||||
Text("All Time").tag(TransactionFilter.Period.all)
|
||||
Text("Today").tag(TransactionFilter.Period.today)
|
||||
Text("This Week").tag(TransactionFilter.Period.week)
|
||||
Text("This Month").tag(TransactionFilter.Period.month)
|
||||
List {
|
||||
// Period Section
|
||||
Section {
|
||||
Picker("Time Period", selection: $selectedPeriodIndex) {
|
||||
ForEach(0..<periods.count, id: \.self) { index in
|
||||
Text(periods[index].name).tag(index)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.onChange(of: selectedPeriodIndex) { newValue in
|
||||
filter.period = periods[newValue].period
|
||||
}
|
||||
} header: {
|
||||
Text("Time Period")
|
||||
}
|
||||
|
||||
// Type Filter
|
||||
Section("Transaction Type") {
|
||||
Picker("Type", selection: Binding(
|
||||
get: { filter.transactionType ?? .expense },
|
||||
set: { filter.transactionType = $0 }
|
||||
)) {
|
||||
Text("All").tag(TransactionType.expense)
|
||||
Text("Income").tag(TransactionType.income)
|
||||
Text("Expense").tag(TransactionType.expense)
|
||||
// Type Section
|
||||
Section {
|
||||
HStack {
|
||||
Text("All")
|
||||
Spacer()
|
||||
if filter.transactionType == nil {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
filter.transactionType = nil
|
||||
}
|
||||
|
||||
HStack {
|
||||
Label("Income", systemImage: "arrow.down.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Spacer()
|
||||
if filter.transactionType == .income {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
filter.transactionType = .income
|
||||
}
|
||||
|
||||
HStack {
|
||||
Label("Expense", systemImage: "arrow.up.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
Spacer()
|
||||
if filter.transactionType == .expense {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
filter.transactionType = .expense
|
||||
}
|
||||
} header: {
|
||||
Text("Transaction Type")
|
||||
}
|
||||
|
||||
// Categories Filter
|
||||
Section("Categories") {
|
||||
// Categories Section
|
||||
Section {
|
||||
ForEach(CategoryModel.categories) { category in
|
||||
Toggle(isOn: Binding(
|
||||
get: { filter.selectedCategories.contains(category.name) },
|
||||
|
|
@ -45,18 +92,30 @@ struct TransactionFilterView: View {
|
|||
HStack {
|
||||
Image(systemName: category.icon)
|
||||
.foregroundColor(Color(category.color))
|
||||
.frame(width: 24)
|
||||
Text(category.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Categories")
|
||||
} footer: {
|
||||
Text("Select categories to filter transactions")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Filters")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Reset") {
|
||||
filter = TransactionFilter()
|
||||
selectedPeriodIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,3 +128,32 @@ struct TransactionFilterView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preview
|
||||
struct TransactionFilterView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
TransactionFilterView(
|
||||
filter: .constant(TransactionFilter())
|
||||
)
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
TransactionFilterView(
|
||||
filter: .constant(TransactionFilter())
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper extension для красивого отображения периодов
|
||||
extension TransactionFilter.Period {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .all: return "All Time"
|
||||
case .today: return "Today"
|
||||
case .week: return "Last 7 Days"
|
||||
case .month: return "Last 30 Days"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,36 +4,60 @@ struct TransactionsView: View {
|
|||
@ObservedObject var store: TransactionsStore
|
||||
@State private var showingAddTransaction = false
|
||||
@State private var showingFilters = false
|
||||
@State private var filter = TransactionFilter()
|
||||
@State private var searchText = ""
|
||||
|
||||
var filteredTransactions: [TransactionModel] {
|
||||
store.transactions.filter { filter.applies(to: $0) }
|
||||
private var groupedTransactions: [(String, [TransactionModel])] {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM yyyy"
|
||||
|
||||
let grouped = Dictionary(grouping: store.transactions) { transaction in
|
||||
formatter.string(from: transaction.date)
|
||||
}
|
||||
|
||||
return grouped.sorted { $0.key > $1.key }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
ForEach(filteredTransactions) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
// Преобразуем индексы отфильтрованного списка в индексы полного списка
|
||||
let transactionsToDelete = indexSet.map { filteredTransactions[$0] }
|
||||
for transaction in transactionsToDelete {
|
||||
if let index = store.transactions.firstIndex(where: { $0.id == transaction.id }) {
|
||||
store.deleteTransaction(at: IndexSet([index]))
|
||||
ZStack {
|
||||
Color(uiColor: .systemBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 24) {
|
||||
ForEach(groupedTransactions, id: \.0) { month, transactions in
|
||||
VStack(spacing: 8) {
|
||||
// Month Header
|
||||
HStack {
|
||||
Text(month)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
.textCase(.uppercase)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Transactions
|
||||
VStack(spacing: 1) {
|
||||
ForEach(transactions) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Transactions")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
showingFilters = true
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.foregroundColor(hasActiveFilters ? .blue : .gray)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,21 +66,55 @@ struct TransactionsView: View {
|
|||
showingAddTransaction = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddTransaction) {
|
||||
AddTransactionView(addTransaction: store.addTransaction)
|
||||
}
|
||||
.sheet(isPresented: $showingFilters) {
|
||||
TransactionFilterView(filter: $filter)
|
||||
}
|
||||
.searchable(
|
||||
text: $searchText,
|
||||
placement: .navigationBarDrawer(displayMode: .always),
|
||||
prompt: "Search transactions"
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showingAddTransaction) {
|
||||
AddTransactionView(addTransaction: store.addTransaction)
|
||||
}
|
||||
.sheet(isPresented: $showingFilters) {
|
||||
TransactionFilterView(filter: .constant(TransactionFilter()))
|
||||
}
|
||||
}
|
||||
|
||||
private var hasActiveFilters: Bool {
|
||||
filter.period != .all ||
|
||||
!filter.selectedCategories.isEmpty ||
|
||||
filter.transactionType != nil
|
||||
}
|
||||
|
||||
// Вспомогательное view для пустого состояния
|
||||
struct EmptyTransactionsView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "creditcard")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color(uiColor: .systemGray))
|
||||
|
||||
Text("No Transactions")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Start adding your transactions to track your expenses and income")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
TransactionsView(store: TransactionsStore())
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
TransactionsView(store: TransactionsStore())
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,65 +10,98 @@ struct AccountCardView: View {
|
|||
|
||||
private var accountColor: Color {
|
||||
switch account.type {
|
||||
case .wallet: return .blue
|
||||
case .bankAccount: return .green
|
||||
case .creditCard: return .purple
|
||||
case .deposit: return .orange
|
||||
case .debt: return .red
|
||||
case .wallet: return Color(uiColor: .systemBlue)
|
||||
case .bankAccount: return Color(uiColor: .systemGreen)
|
||||
case .creditCard: return Color(uiColor: .systemPurple)
|
||||
case .deposit: return Color(uiColor: .systemOrange)
|
||||
case .debt: return Color(uiColor: .systemRed)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(spacing: AppStyle.paddingMedium) {
|
||||
// Header
|
||||
HStack {
|
||||
HStack(alignment: .center) {
|
||||
// Icon
|
||||
Image(systemName: accountIcon)
|
||||
.font(.title2)
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundColor(accountColor)
|
||||
.frame(width: 32)
|
||||
|
||||
Text(account.name)
|
||||
.font(.headline)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(account.name)
|
||||
.font(AppStyle.fontHeadline)
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
|
||||
Text(account.type.rawValue)
|
||||
.font(AppStyle.fontSubheadline)
|
||||
.foregroundColor(AppStyle.labelSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(account.currency.symbol)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
.font(AppStyle.fontSubheadline)
|
||||
.foregroundColor(AppStyle.labelSecondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal, -AppStyle.paddingMedium)
|
||||
|
||||
// Balance
|
||||
Text(account.balance.formatAsCurrency())
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Balance")
|
||||
.font(AppStyle.fontSubheadline)
|
||||
.foregroundColor(AppStyle.labelSecondary)
|
||||
|
||||
Text(account.balance.formatAsCurrency())
|
||||
.font(AppStyle.fontTitle2)
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
// Additional Info
|
||||
if let availableCredit = account.availableCredit {
|
||||
Text("Available: \(availableCredit.formatAsCurrency())")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
HStack {
|
||||
Text("Available Credit")
|
||||
.font(AppStyle.fontFootnote)
|
||||
.foregroundColor(AppStyle.labelSecondary)
|
||||
Spacer()
|
||||
Text(availableCredit.formatAsCurrency())
|
||||
.font(AppStyle.fontCallout)
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
if account.isOverdue {
|
||||
Label("Overdue", systemImage: "exclamationmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundColor(Color(uiColor: .systemRed))
|
||||
Text("Payment Overdue")
|
||||
.font(AppStyle.fontFootnote)
|
||||
.foregroundColor(Color(uiColor: .systemRed))
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
.padding(AppStyle.paddingMedium)
|
||||
.cardStyle()
|
||||
}
|
||||
}
|
||||
|
||||
// Preview
|
||||
struct AccountCardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AccountCardView(account: AccountModel.sampleData[0])
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
AccountCardView(account: AccountModel.sampleData[2])
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
.background(AppStyle.backgroundPrimary)
|
||||
.environmentObject(AppSettings.shared)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,58 +10,116 @@ struct TransactionRowView: View {
|
|||
|
||||
private var categoryColor: Color {
|
||||
switch category.color {
|
||||
case "red": return .red
|
||||
case "blue": return .blue
|
||||
case "purple": return .purple
|
||||
case "orange": return .orange
|
||||
case "green": return .green
|
||||
case "gray": return .gray
|
||||
default: return .gray
|
||||
case "red": return Color(uiColor: .systemRed)
|
||||
case "blue": return Color(uiColor: .systemBlue)
|
||||
case "purple": return Color(uiColor: .systemPurple)
|
||||
case "orange": return Color(uiColor: .systemOrange)
|
||||
case "green": return Color(uiColor: .systemGreen)
|
||||
case "gray": return Color(uiColor: .systemGray)
|
||||
default: return Color(uiColor: .systemGray)
|
||||
}
|
||||
}
|
||||
|
||||
let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "d MMM yyyy"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: AppStyle.paddingMedium) {
|
||||
// Category Icon
|
||||
Image(systemName: category.icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(categoryColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(categoryColor)
|
||||
.frame(width: 36, height: 36)
|
||||
|
||||
Image(systemName: category.icon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// Transaction Details
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(transaction.category)
|
||||
.font(.headline)
|
||||
if let note = transaction.note {
|
||||
Text(note)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
HStack {
|
||||
Text(transaction.category)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(AppStyle.labelPrimary)
|
||||
|
||||
if transaction.isExpense {
|
||||
Text("Expense")
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Color(uiColor: .systemRed))
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text(dateFormatter.string(from: transaction.date))
|
||||
|
||||
if let note = transaction.note {
|
||||
Text("•")
|
||||
Text(note)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(Color(uiColor: .systemGray))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Amount and Date
|
||||
// Amount
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(transaction.amountInCurrentCurrency().formatAsCurrency())
|
||||
.font(.headline)
|
||||
.foregroundColor(transaction.isExpense ? .red : .green)
|
||||
Text(transaction.date, style: .date)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundColor(transaction.isExpense ?
|
||||
Color(uiColor: .systemRed) :
|
||||
Color(uiColor: .systemGreen))
|
||||
|
||||
Text(transaction.originalCurrency.rawValue)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(Color(uiColor: .systemGray))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, AppStyle.paddingMedium)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionRowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TransactionRowView(transaction: TransactionModel.sampleData[0])
|
||||
.previewLayout(.sizeThatFits)
|
||||
.padding()
|
||||
.environmentObject(AppSettings.shared)
|
||||
VStack(spacing: 8) {
|
||||
TransactionRowView(transaction: TransactionModel(
|
||||
amount: 100.00,
|
||||
date: Date(),
|
||||
type: .income,
|
||||
category: "Salary",
|
||||
note: "Monthly salary",
|
||||
originalCurrency: .usd
|
||||
))
|
||||
|
||||
TransactionRowView(transaction: TransactionModel(
|
||||
amount: 25.99,
|
||||
date: Date(),
|
||||
type: .expense,
|
||||
category: "Food",
|
||||
note: "Lunch",
|
||||
originalCurrency: .usd
|
||||
))
|
||||
|
||||
TransactionRowView(transaction: TransactionModel(
|
||||
amount: 50.00,
|
||||
date: Date(),
|
||||
type: .expense,
|
||||
category: "Transport",
|
||||
note: "Fuel",
|
||||
originalCurrency: .usd
|
||||
))
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.environmentObject(AppSettings.shared)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue