Redesign dashboard with Apple-style UI improvements, better account cards and spending analysis
This commit is contained in:
parent
c3f0a87841
commit
78c47819d0
6 changed files with 223 additions and 145 deletions
|
|
@ -26,11 +26,18 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
.environmentObject(settings)
|
||||
.preferredColorScheme(settings.isDarkMode ? .dark : .light)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
Group {
|
||||
ContentView()
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
ContentView()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ struct AccountsListView: View {
|
|||
Section(type.rawValue) {
|
||||
ForEach(store.getAccounts(of: type)) { account in
|
||||
AccountCardView(account: account)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,13 +43,20 @@ struct AccountsListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountsListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountsListView(store: AccountsStore())
|
||||
.environmentObject(AppSettings.shared)
|
||||
Group {
|
||||
AccountsListView(store: AccountsStore())
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
AccountsListView(store: AccountsStore())
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ struct DashboardView: View {
|
|||
@ObservedObject var store: TransactionsStore
|
||||
@ObservedObject var accountsStore: AccountsStore
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
@State private var showingAccountsList = false
|
||||
|
||||
private var totalBalance: Double {
|
||||
store.transactions.reduce(0) { total, transaction in
|
||||
|
|
@ -42,129 +43,191 @@ struct DashboardView: View {
|
|||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// Balance Card
|
||||
VStack(spacing: 8) {
|
||||
Text("Total Balance")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text(totalBalance.formatAsCurrency())
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
|
||||
// Accounts Summary
|
||||
AccountsSummaryView(accountsStore: accountsStore)
|
||||
|
||||
// Income/Expense Summary
|
||||
HStack(spacing: 16) {
|
||||
// Income Card
|
||||
VStack(spacing: 8) {
|
||||
Text("Income")
|
||||
VStack(spacing: 24) {
|
||||
// Header Stats
|
||||
HStack(spacing: 20) {
|
||||
// Monthly Balance
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("This Month")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
Text(income.formatAsCurrency())
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
|
||||
// Expense Card
|
||||
VStack(spacing: 8) {
|
||||
Text("Expenses")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
Text(expenses.formatAsCurrency())
|
||||
.font(.headline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
}
|
||||
|
||||
// Expenses by Category
|
||||
if !categoryExpenses.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Expenses by Category")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
Text(totalBalance.formatAsCurrency())
|
||||
.font(.system(size: 28, weight: .semibold))
|
||||
|
||||
// Pie Chart
|
||||
PieChartView(slices: categoryExpenses)
|
||||
.frame(height: 200)
|
||||
.padding(.vertical)
|
||||
|
||||
// Category Legend
|
||||
VStack(spacing: 12) {
|
||||
ForEach(categoryExpenses) { slice in
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color(slice.category.color))
|
||||
.frame(width: 12, height: 12)
|
||||
|
||||
Text(slice.category.name)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(slice.amount.formatAsCurrency())
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(String(format: "(%.1f%%)", slice.percentage * 100))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
HStack(spacing: 16) {
|
||||
// Income indicator
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 8, height: 8)
|
||||
Text("↑ \(income.formatAsCurrency())")
|
||||
.font(.caption)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
// Expense indicator
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
Text("↓ \(expenses.formatAsCurrency())")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(16)
|
||||
|
||||
// Recent Transactions
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Recent Transactions")
|
||||
.font(.headline)
|
||||
// Accounts Section
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Accounts")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
Button("See All") {
|
||||
showingAccountsList = true
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
ForEach(Array(store.transactions.prefix(3))) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
if transaction.id != store.transactions.prefix(3).last?.id {
|
||||
Divider()
|
||||
ForEach(Array(accountsStore.accounts.prefix(3))) { account in
|
||||
Button(action: {
|
||||
// Show account details
|
||||
}) {
|
||||
HStack(spacing: 16) {
|
||||
// Account Icon
|
||||
Image(systemName: account.type.icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(accountColor(for: account.type))
|
||||
.cornerRadius(12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(account.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color(uiColor: .label))
|
||||
Text(account.type.rawValue)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(account.balance.formatAsCurrency())
|
||||
.font(.headline)
|
||||
.foregroundColor(Color(uiColor: .label))
|
||||
if let availableCredit = account.availableCredit {
|
||||
Text("Available: \(availableCredit.formatAsCurrency())")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
.padding(.horizontal)
|
||||
|
||||
// Expenses Analysis
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Spending Analysis")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(categoryExpenses) { slice in
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: slice.category.icon)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(Color(slice.category.color))
|
||||
.cornerRadius(8)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(slice.category.name)
|
||||
.font(.headline)
|
||||
Text(slice.amount.formatAsCurrency())
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Rectangle()
|
||||
.fill(Color(uiColor: .systemFill))
|
||||
Rectangle()
|
||||
.fill(Color(slice.category.color))
|
||||
.frame(width: geometry.size.width * slice.percentage)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
.cornerRadius(4)
|
||||
|
||||
Text(String(format: "%.1f%%", slice.percentage * 100))
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
.frame(width: 160)
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.vertical)
|
||||
}
|
||||
.navigationTitle("Dashboard")
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.sheet(isPresented: $showingAccountsList) {
|
||||
AccountsListView(store: accountsStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func accountColor(for type: AccountType) -> Color {
|
||||
switch type {
|
||||
case .wallet: return .blue
|
||||
case .bankAccount: return .green
|
||||
case .creditCard: return .purple
|
||||
case .deposit: return .orange
|
||||
case .debt: return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DashboardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DashboardView(
|
||||
store: TransactionsStore(),
|
||||
accountsStore: AccountsStore()
|
||||
)
|
||||
Group {
|
||||
DashboardView(
|
||||
store: TransactionsStore(),
|
||||
accountsStore: AccountsStore()
|
||||
)
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
DashboardView(
|
||||
store: TransactionsStore(),
|
||||
accountsStore: AccountsStore()
|
||||
)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class AppSettings: ObservableObject {
|
||||
@Published var currency: Currency = .usd {
|
||||
|
|
@ -8,7 +8,11 @@ class AppSettings: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
@Published var isDarkMode: Bool = false
|
||||
@AppStorage("isDarkMode") var isDarkMode: Bool = false {
|
||||
didSet {
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
@Published var notificationsEnabled: Bool = true
|
||||
@Published private(set) var exchangeRates: [String: Double] = [:]
|
||||
@Published private(set) var lastRatesUpdate: Date?
|
||||
|
|
@ -30,13 +34,21 @@ class AppSettings: ObservableObject {
|
|||
static let shared = AppSettings()
|
||||
|
||||
private init() {
|
||||
// Инициализируем базовые курсы (1 USD = ...)
|
||||
exchangeRates = [
|
||||
"USD": 1.0, // 1 USD = 1 USD
|
||||
"EUR": 0.92, // 1 USD = 0.92 EUR
|
||||
"GBP": 0.79 // 1 USD = 0.79 GBP
|
||||
"USD": 1.0,
|
||||
"EUR": 0.92,
|
||||
"GBP": 0.79
|
||||
]
|
||||
fetchExchangeRates()
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
private func applyTheme() {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
windowScene.windows.forEach { window in
|
||||
window.overrideUserInterfaceStyle = isDarkMode ? .dark : .light
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convert(_ amount: Double, from sourceCurrency: Currency, to targetCurrency: Currency) -> Double {
|
||||
|
|
@ -47,13 +59,11 @@ class AppSettings: ObservableObject {
|
|||
return amount
|
||||
}
|
||||
|
||||
// Сначала конвертируем в USD, затем в целевую валюту
|
||||
if sourceCurrency == .usd {
|
||||
return amount * targetRate
|
||||
} else if targetCurrency == .usd {
|
||||
return amount / sourceRate
|
||||
} else {
|
||||
// Конвертация через USD
|
||||
let amountInUSD = amount / sourceRate
|
||||
return amountInUSD * targetRate
|
||||
}
|
||||
|
|
@ -73,7 +83,7 @@ class AppSettings: ObservableObject {
|
|||
let response = try JSONDecoder().decode(ExchangeRatesResponse.self, from: data)
|
||||
DispatchQueue.main.async {
|
||||
var rates = response.rates
|
||||
rates["USD"] = 1.0 // Базовая валюта всегда 1.0
|
||||
rates["USD"] = 1.0
|
||||
|
||||
self?.exchangeRates = rates
|
||||
self?.lastRatesUpdate = Date()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
//
|
||||
// AccountCardView.swift
|
||||
// Coinly
|
||||
//
|
||||
// Created by Vadym Samoilenko on 02/03/2025.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AccountCardView: View {
|
||||
|
|
@ -41,7 +33,7 @@ struct AccountCardView: View {
|
|||
|
||||
Text(account.currency.symbol)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
|
||||
// Balance
|
||||
|
|
@ -53,7 +45,7 @@ struct AccountCardView: View {
|
|||
if let availableCredit = account.availableCredit {
|
||||
Text("Available: \(availableCredit.formatAsCurrency())")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
}
|
||||
|
||||
if account.isOverdue {
|
||||
|
|
@ -63,7 +55,7 @@ struct AccountCardView: View {
|
|||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
}
|
||||
|
|
@ -76,8 +68,8 @@ struct AccountCardView_Previews: PreviewProvider {
|
|||
AccountCardView(account: AccountModel.sampleData[2])
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
.environmentObject(AppSettings.shared)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
//
|
||||
// AccountsSummaryView.swift
|
||||
// Coinly
|
||||
//
|
||||
// Created by Vadym Samoilenko on 02/03/2025.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AccountsSummaryView: View {
|
||||
|
|
@ -23,7 +15,7 @@ struct AccountsSummaryView: View {
|
|||
VStack(spacing: 8) {
|
||||
Text("Total Assets")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
.foregroundColor(Color(uiColor: .secondaryLabel))
|
||||
|
||||
Text(totalBalance.formatAsCurrency())
|
||||
.font(.title2)
|
||||
|
|
@ -47,7 +39,7 @@ struct AccountsSummaryView: View {
|
|||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(radius: 2)
|
||||
.sheet(isPresented: $showingAccountsList) {
|
||||
|
|
@ -58,10 +50,16 @@ struct AccountsSummaryView: View {
|
|||
|
||||
struct AccountsSummaryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountsSummaryView(accountsStore: AccountsStore())
|
||||
.environmentObject(AppSettings.shared)
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.previewLayout(.sizeThatFits)
|
||||
Group {
|
||||
AccountsSummaryView(accountsStore: AccountsStore())
|
||||
.preferredColorScheme(.light)
|
||||
|
||||
AccountsSummaryView(accountsStore: AccountsStore())
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
.environmentObject(AppSettings.shared)
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue