Remove Add Transaction feature and clean all related code

Deleted files:
- AddTransactionView.swift
- TransactionRepository.swift
- Transaction+CoreDataClass.swift

Cleaned files:
- SourceDetailViewModel: removed transactions published property,
  showingAddTransaction flag, transactionRepository dependency,
  addTransaction() and deleteTransaction() methods
- SourceDetailView: removed transactionsSection, TransactionRow struct,
  Add Transaction button from quickActions, sheet presentation
- InvestmentSource+CoreDataClass: removed transactions NSManaged property,
  transactionsArray, totalInvested, totalDividends, totalFees computed
  properties, and all transaction Core Data accessors
- SettingsViewModel: removed "Transaction" from resetAllData entity list
- SampleDataService: removed transactionRepository and sample transaction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-20 23:04:03 +01:00
parent b17d8866a4
commit 7cb5f92cf4
20 changed files with 215 additions and 503 deletions
Vendored
BIN
View File
Binary file not shown.
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>2825Q76T7H</string>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
@@ -16,7 +16,6 @@ public class InvestmentSource: NSManagedObject, Identifiable {
@NSManaged public var category: Category?
@NSManaged public var account: Account?
@NSManaged public var snapshots: NSSet?
@NSManaged public var transactions: NSSet?
@NSManaged public var asset: Asset?
public override func awakeFromInsert() {
@@ -104,13 +103,6 @@ extension InvestmentSource {
snapshots?.count ?? 0
}
/// Returns transactions sorted by date descending
/// Performance note: This sorts on every call. For repeated access, cache the result.
var transactionsArray: [Transaction] {
let set = transactions as? Set<Transaction> ?? []
return set.sorted { $0.date > $1.date }
}
var frequency: NotificationFrequency {
NotificationFrequency(rawValue: notificationFrequency) ?? .monthly
}
@@ -168,37 +160,6 @@ extension InvestmentSource {
}
}
/// Performance: Iterates transactions directly without sorting
var totalInvested: Decimal {
guard let set = transactions as? Set<Transaction> else { return Decimal.zero }
return set.reduce(Decimal.zero) { result, transaction in
let amount = transaction.decimalAmount
switch transaction.transactionType {
case .buy:
return result + amount
case .sell:
return result - amount
default:
return result
}
}
}
/// Performance: Iterates transactions directly without sorting
var totalDividends: Decimal {
guard let set = transactions as? Set<Transaction> else { return Decimal.zero }
return set.reduce(Decimal.zero) { result, transaction in
transaction.transactionType == .dividend ? result + transaction.decimalAmount : result
}
}
/// Performance: Iterates transactions directly without sorting
var totalFees: Decimal {
guard let set = transactions as? Set<Transaction> else { return Decimal.zero }
return set.reduce(Decimal.zero) { result, transaction in
transaction.transactionType == .fee ? result + transaction.decimalAmount : result
}
}
}
// MARK: - Account Scheduling
@@ -233,15 +194,4 @@ extension InvestmentSource {
@objc(removeSnapshots:)
@NSManaged public func removeFromSnapshots(_ values: NSSet)
@objc(addTransactionsObject:)
@NSManaged public func addToTransactions(_ value: Transaction)
@objc(removeTransactionsObject:)
@NSManaged public func removeFromTransactions(_ value: Transaction)
@objc(addTransactions:)
@NSManaged public func addToTransactions(_ values: NSSet)
@objc(removeTransactions:)
@NSManaged public func removeFromTransactions(_ values: NSSet)
}
@@ -1,75 +0,0 @@
import Foundation
import CoreData
@objc(Transaction)
public class Transaction: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Transaction> {
return NSFetchRequest<Transaction>(entityName: "Transaction")
}
@NSManaged public var id: UUID
@NSManaged public var date: Date
@NSManaged public var type: String
@NSManaged public var shares: NSDecimalNumber?
@NSManaged public var price: NSDecimalNumber?
@NSManaged public var amount: NSDecimalNumber?
@NSManaged public var notes: String?
@NSManaged public var createdAt: Date
@NSManaged public var source: InvestmentSource?
public override func awakeFromInsert() {
super.awakeFromInsert()
id = UUID()
createdAt = Date()
date = Date()
type = TransactionType.buy.rawValue
}
}
enum TransactionType: String, CaseIterable, Identifiable {
case buy
case sell
case dividend
case fee
case transfer
var id: String { rawValue }
var displayName: String {
switch self {
case .buy: return "Buy"
case .sell: return "Sell"
case .dividend: return "Dividend"
case .fee: return "Fee"
case .transfer: return "Transfer"
}
}
var isInvestmentFlow: Bool {
self == .buy || self == .sell
}
}
// MARK: - Computed Properties
extension Transaction {
var decimalShares: Decimal {
shares?.decimalValue ?? Decimal.zero
}
var decimalPrice: Decimal {
price?.decimalValue ?? Decimal.zero
}
var decimalAmount: Decimal {
if let amount = amount?.decimalValue, amount != 0 {
return amount
}
return decimalShares * decimalPrice
}
var transactionType: TransactionType {
TransactionType(rawValue: type) ?? .buy
}
}
@@ -15,6 +15,12 @@ struct MonthlySummary {
return NSDecimalNumber(decimal: netPerformance / base).doubleValue * 100
}
var formattedMonthYear: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
return formatter.string(from: startDate)
}
var formattedStartingValue: String {
CurrencyFormatter.format(startingValue, style: .currency, maximumFractionDigits: 0)
}
@@ -1,62 +0,0 @@
import Foundation
import CoreData
class TransactionRepository {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) {
self.context = context
}
func fetchTransactions(for source: InvestmentSource) -> [Transaction] {
let request: NSFetchRequest<Transaction> = Transaction.fetchRequest()
request.predicate = NSPredicate(format: "source == %@", source)
request.sortDescriptors = [
NSSortDescriptor(keyPath: \Transaction.date, ascending: false)
]
return (try? context.fetch(request)) ?? []
}
@discardableResult
func createTransaction(
source: InvestmentSource,
type: TransactionType,
date: Date,
shares: Decimal?,
price: Decimal?,
amount: Decimal?,
notes: String?
) -> Transaction {
let transaction = Transaction(context: context)
transaction.source = source
transaction.type = type.rawValue
transaction.date = date
if let shares = shares {
transaction.shares = NSDecimalNumber(decimal: shares)
}
if let price = price {
transaction.price = NSDecimalNumber(decimal: price)
}
if let amount = amount {
transaction.amount = NSDecimalNumber(decimal: amount)
}
transaction.notes = notes
save()
return transaction
}
func deleteTransaction(_ transaction: Transaction) {
context.delete(transaction)
save()
}
private func save() {
guard context.hasChanges else { return }
do {
try context.save()
} catch {
print("Failed to save transaction: \(error)")
}
}
}
@@ -19,7 +19,6 @@ class SampleDataService {
let snapshotRepository = SnapshotRepository(context: context)
let goalRepository = GoalRepository(context: context)
let transactionRepository = TransactionRepository(context: context)
let categories = fetchCategories(in: context)
guard let fallbackCategory = categories.first else { return }
@@ -70,16 +69,6 @@ class SampleDataService {
seedMonthlyNotes()
transactionRepository.createTransaction(
source: stocks,
type: .buy,
date: Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date(),
shares: 10,
price: 400,
amount: nil,
notes: "Sample buy"
)
_ = goalRepository.createGoal(
name: "1M Goal",
targetAmount: 1_000_000,
@@ -105,6 +105,28 @@ extension Color {
return Color(hex: hex) ?? .blue
}
// MARK: - Source Colors (distinct per-source palette, different order from categories)
static let sourceColors: [String] = [
"#6366F1", // Indigo
"#F97316", // Orange
"#06B6D4", // Cyan
"#EF4444", // Red
"#84CC16", // Lime
"#EC4899", // Pink
"#14B8A6", // Teal
"#F59E0B", // Amber
"#8B5CF6", // Purple
"#3B82F6", // Blue
"#A855F7", // Violet
"#10B981", // Green
]
static func sourceColor(at index: Int) -> Color {
let hex = sourceColors[index % sourceColors.count]
return Color(hex: hex) ?? .blue
}
// MARK: - Chart Colors
static let chartColors: [Color] = categoryColors.compactMap { Color(hex: $0) }
@@ -136,6 +136,18 @@ extension Date {
return formatter.localizedString(for: self, relativeTo: Date())
}
var relativeDayDescription: String {
let calendar = Calendar.current
let days = calendar.dateComponents([.day], from: calendar.startOfDay(for: self), to: calendar.startOfDay(for: Date())).day ?? 0
if days == 0 { return String(localized: "date_today") }
if days == 1 { return "1d ago" }
if days < 31 { return "\(days)d ago" }
let months = calendar.dateComponents([.month], from: self, to: Date()).month ?? 0
if months < 12 { return "\(max(1, months))mo ago" }
let years = calendar.dateComponents([.year], from: self, to: Date()).year ?? 0
return "\(max(1, years))y ago"
}
var friendlyDescription: String {
if isToday {
return String(localized: "date_today")
@@ -89,8 +89,7 @@ class MonthlyCheckInViewModel: ObservableObject {
)
let targetSourceIds = Set(targetSnapshots.compactMap { $0.source?.id })
let now = Date()
let targetDate = targetRange.contains(now) ? now : targetRange.end
let targetDate = Date()
for source in sources {
let sourceId = source.id
@@ -316,7 +316,6 @@ class SettingsViewModel: ObservableObject {
"Asset",
"Account",
"Goal",
"Transaction",
"PredictionCache"
]
@@ -11,14 +11,12 @@ class SourceDetailViewModel: ObservableObject {
@Published var metrics: InvestmentMetrics = .empty
@Published var predictions: [Prediction] = []
@Published var predictionResult: PredictionResult?
@Published var transactions: [Transaction] = []
@Published var isDeleted = false
@Published var isLoading = false
@Published var showingAddSnapshot = false
@Published var showingEditSource = false
@Published var showingPaywall = false
@Published var showingAddTransaction = false
@Published var errorMessage: String?
// MARK: - Chart Data
@@ -29,7 +27,6 @@ class SourceDetailViewModel: ObservableObject {
private let snapshotRepository: SnapshotRepository
private let sourceRepository: InvestmentSourceRepository
private let transactionRepository: TransactionRepository
private let calculationService: CalculationService
private let predictionEngine: PredictionEngine
private let freemiumValidator: FreemiumValidator
@@ -46,7 +43,6 @@ class SourceDetailViewModel: ObservableObject {
source: InvestmentSource,
snapshotRepository: SnapshotRepository? = nil,
sourceRepository: InvestmentSourceRepository? = nil,
transactionRepository: TransactionRepository? = nil,
calculationService: CalculationService? = nil,
predictionEngine: PredictionEngine? = nil,
iapService: IAPService
@@ -55,7 +51,6 @@ class SourceDetailViewModel: ObservableObject {
self.sourceName = source.name
self.snapshotRepository = snapshotRepository ?? SnapshotRepository()
self.sourceRepository = sourceRepository ?? InvestmentSourceRepository()
self.transactionRepository = transactionRepository ?? TransactionRepository()
self.calculationService = calculationService ?? .shared
self.predictionEngine = predictionEngine ?? .shared
self.freemiumValidator = FreemiumValidator(iapService: iapService)
@@ -147,8 +142,6 @@ class SourceDetailViewModel: ObservableObject {
}
}
// Transactions update independently
self.transactions = transactionRepository.fetchTransactions(for: source)
}
self.isRefreshing = false
}
@@ -191,33 +184,6 @@ class SourceDetailViewModel: ObservableObject {
refreshData()
}
// MARK: - Transaction Actions
func addTransaction(
type: TransactionType,
date: Date,
shares: Decimal?,
price: Decimal?,
amount: Decimal?,
notes: String?
) {
transactionRepository.createTransaction(
source: source,
type: type,
date: date,
shares: shares,
price: price,
amount: amount,
notes: notes
)
refreshData()
}
func deleteTransaction(_ transaction: Transaction) {
transactionRepository.deleteTransaction(transaction)
refreshData()
}
// MARK: - Source Actions
func updateSource(
@@ -22,28 +22,8 @@ struct ChartsContainerView: View {
// Chart Type Selector
chartTypeSelector
// Time Range Selector
if viewModel.selectedChartType != .allocation &&
viewModel.selectedChartType != .riskReturn {
timeRangeSelector
}
// Category Filter
if viewModel.selectedChartType == .evolution ||
viewModel.selectedChartType == .prediction {
categoryFilter
}
// Source Filter
if viewModel.selectedChartType == .evolution {
sourceFilter
}
// Breakdown Selector
if viewModel.selectedChartType == .allocation ||
viewModel.selectedChartType == .performance {
breakdownSelector
}
// Unified Filters
filtersSection
// Chart Content
chartContent
@@ -108,6 +88,74 @@ struct ChartsContainerView: View {
}
}
// MARK: - Unified Filters Section
private var hasAnyFilter: Bool {
let chartType = viewModel.selectedChartType
let hasTimeRange = chartType != .allocation && chartType != .riskReturn
let hasCategories = !viewModel.availableCategories(for: chartType).isEmpty
let hasSources = !viewModel.availableSources(for: chartType).isEmpty
let hasBreakdown = chartType == .allocation || chartType == .performance
return hasTimeRange || hasCategories || hasSources || hasBreakdown
}
@ViewBuilder
private var filtersSection: some View {
if hasAnyFilter {
VStack(alignment: .leading, spacing: 10) {
let chartType = viewModel.selectedChartType
// Time Range
if chartType != .allocation && chartType != .riskReturn {
filterRow(icon: "calendar", label: "Period") {
timeRangeSelector
}
}
// Breakdown (category vs source)
if chartType == .allocation || chartType == .performance {
filterRow(icon: "square.grid.2x2", label: "Group") {
breakdownSelector
}
}
// Category
let availableCategories = viewModel.availableCategories(for: chartType)
if !availableCategories.isEmpty {
filterRow(icon: "tag", label: "Category") {
categoryFilter
}
}
// Source
let availableSources = viewModel.availableSources(for: chartType)
if !availableSources.isEmpty {
filterRow(icon: "building.2", label: "Source") {
sourceFilter
}
}
}
.padding(12)
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 4, y: 1)
}
}
private func filterRow<Content: View>(icon: String, label: String, @ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.caption2)
.foregroundColor(.secondary)
Text(label)
.font(.caption.weight(.medium))
.foregroundColor(.secondary)
}
content()
}
}
// MARK: - Time Range Selector
private var timeRangeSelector: some View {
@@ -229,26 +277,31 @@ struct ChartsContainerView: View {
}
}
ForEach(availableSources, id: \.id) { source in
let sourceId = source.id ?? UUID()
ForEach(Array(availableSources.enumerated()), id: \.element.id) { index, source in
let sourceId = source.id
let isSelected = viewModel.selectedSourceIds.contains(sourceId)
let sourceColor = Color.sourceColor(at: index)
Button {
viewModel.selectedSource = nil
viewModel.selectedCategory = nil
if isSelected {
viewModel.selectedSourceIds.remove(sourceId)
} else {
viewModel.selectedSourceIds.insert(sourceId)
}
} label: {
Text(source.name)
.font(.caption.weight(.medium))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
isSelected
? Color.appPrimary
: Color.gray.opacity(0.1)
HStack(spacing: 4) {
Circle()
.fill(sourceColor)
.frame(width: 8, height: 8)
Text(source.name)
}
.font(.caption.weight(.medium))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
isSelected
? sourceColor
: Color.gray.opacity(0.1)
)
.foregroundColor(
isSelected
@@ -975,15 +1028,36 @@ struct AllocationEvolutionChart: View {
.frame(height: 260)
}
/// Categories in stable order (preserving the ViewModel's sort: largest overall first)
private var stableCategoryNames: [String] {
var seen = Set<String>()
var ordered: [String] = []
for item in data {
if seen.insert(item.category).inserted {
ordered.append(item.category)
}
}
return ordered
}
private var stableCategoryColors: [Color] {
stableCategoryNames.map { name in
if let hex = data.first(where: { $0.category == name })?.color {
return Color(hex: hex) ?? .gray
}
return .gray
}
}
private var chartView: some View {
Chart(identifiableData) { item in
AreaMark(
x: .value("Date", item.date),
BarMark(
x: .value("Date", item.date, unit: .month),
y: .value("Percentage", item.percentage)
)
.foregroundStyle(by: .value("Category", item.category))
}
.chartForegroundStyleScale(domain: categoryNames, range: categoryColors)
.chartForegroundStyleScale(domain: stableCategoryNames, range: stableCategoryColors)
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 2)) { _ in
AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits))
@@ -1002,19 +1076,6 @@ struct AllocationEvolutionChart: View {
.frame(height: 260)
.drawingGroup()
}
private var categoryNames: [String] {
Array(Set(data.map { $0.category })).sorted()
}
private var categoryColors: [Color] {
categoryNames.map { name in
if let hex = data.first(where: { $0.category == name })?.color {
return Color(hex: hex) ?? .gray
}
return .gray
}
}
}
#Preview {
@@ -416,9 +416,10 @@ struct MonthlyCheckInCard: View {
@State private var showingStartOptions = false
@State private var startDestinationActive = false
@State private var shouldDuplicatePrevious = false
@State private var cachedCompletionDate: Date?
private var effectiveLastCheckInDate: Date? {
MonthlyCheckInStore.latestCompletionDate() ?? lastUpdatedDate
cachedCompletionDate ?? lastUpdatedDate
}
private var checkInProgress: Double {
@@ -432,7 +433,8 @@ struct MonthlyCheckInCard: View {
private var nextCheckInDate: Date? {
guard let last = effectiveLastCheckInDate else { return nil }
return last.adding(months: 1)
let effective = MonthlyCheckInStore.effectiveMonth(for: last, relativeTo: last)
return effective.adding(months: 1).endOfMonth
}
private var isOverdue: Bool {
@@ -473,10 +475,10 @@ struct MonthlyCheckInCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(currentMonthLabel)
Text("Monthly Check-in")
.font(.headline)
Text("Keep a calm, deliberate rhythm. Update your sources and add a short note.")
Text(currentMonthLabel)
.font(.subheadline)
.foregroundColor(.secondary)
@@ -583,6 +585,14 @@ struct MonthlyCheckInCard: View {
.padding(.horizontal, 24)
.presentationDetents([.height(280)])
}
.onAppear {
cachedCompletionDate = MonthlyCheckInStore.latestCompletionDate()
}
.onChange(of: startDestinationActive) { _, isActive in
if !isActive {
cachedCompletionDate = MonthlyCheckInStore.latestCompletionDate()
}
}
}
private var defaultReminderTime: Date {
@@ -7,7 +7,7 @@ struct MonthlyCheckInShareCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 14) {
Text(summary.periodLabel)
Text(summary.formattedMonthYear)
.font(.caption.weight(.semibold))
.foregroundColor(.white.opacity(0.85))
@@ -17,7 +17,9 @@ struct MonthlyCheckInShareCardView: View {
metricRow("Starting", summary.formattedStartingValue)
metricRow("Ending", summary.formattedEndingValue)
metricRow("Contributions", summary.formattedContributions)
if summary.contributions != 0 {
metricRow("Contributions", summary.formattedContributions)
}
metricRow("Net performance", "\(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage))")
Spacer(minLength: 0)
@@ -106,13 +106,31 @@ struct GoalEditorView: View {
private func parseDecimal(_ value: String) -> Decimal? {
let locale = CurrencyFormatter.locale(for: currencyCode)
let cleaned = value
let stripped = value
.replacingOccurrences(of: currencySymbol, with: "")
.replacingOccurrences(of: locale.groupingSeparator ?? "", with: "")
.trimmingCharacters(in: .whitespaces)
guard !cleaned.isEmpty else { return nil }
guard !stripped.isEmpty else { return nil }
let decimalSep = locale.decimalSeparator ?? "."
let groupingSep = locale.groupingSeparator ?? ""
// Detect alternate decimal BEFORE removing separators
let usesAlternateDecimal =
(decimalSep == "," && stripped.contains(".") && !stripped.contains(",")) ||
(decimalSep == "." && stripped.contains(",") && !stripped.contains("."))
if usesAlternateDecimal {
let normalized = stripped
.replacingOccurrences(of: groupingSep, with: "")
.replacingOccurrences(of: ",", with: ".")
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.number(from: normalized)?.decimalValue
}
let cleaned = stripped.replacingOccurrences(of: groupingSep, with: "")
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = locale
@@ -123,7 +141,7 @@ struct GoalEditorView: View {
// Fallback for mixed locale input
let normalized = cleaned
.replacingOccurrences(of: locale.decimalSeparator ?? ",", with: ".")
.replacingOccurrences(of: decimalSep, with: ".")
.replacingOccurrences(of: ",", with: ".")
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.number(from: normalized)?.decimalValue
@@ -1,94 +0,0 @@
import SwiftUI
struct AddTransactionView: View {
@Environment(\.dismiss) private var dismiss
let onSave: (TransactionType, Date, Decimal?, Decimal?, Decimal?, String?) -> Void
@State private var type: TransactionType = .buy
@State private var date = Date()
@State private var shares = ""
@State private var price = ""
@State private var amount = ""
@State private var notes = ""
var body: some View {
NavigationStack {
Form {
Section {
Picker("Type", selection: $type) {
ForEach(TransactionType.allCases) { transactionType in
Text(transactionType.displayName).tag(transactionType)
}
}
DatePicker("Date", selection: $date, displayedComponents: .date)
} header: {
Text("Transaction")
}
Section {
TextField("Shares", text: $shares)
.keyboardType(.decimalPad)
TextField("Price per share", text: $price)
.keyboardType(.decimalPad)
TextField("Total amount", text: $amount)
.keyboardType(.decimalPad)
} header: {
Text("Amounts")
} footer: {
Text("Enter shares and price or just a total amount.")
}
Section {
TextField("Notes", text: $notes, axis: .vertical)
.lineLimit(2...4)
} header: {
Text("Notes (Optional)")
}
}
.navigationTitle("Add Transaction")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { save() }
.disabled(!isValid)
}
}
}
.presentationDetents([.medium, .large])
}
private var isValid: Bool {
parseDecimal(shares) != nil || parseDecimal(amount) != nil
}
private func save() {
onSave(
type,
date,
parseDecimal(shares),
parseDecimal(price),
parseDecimal(amount),
notes.isEmpty ? nil : notes
)
dismiss()
}
private func parseDecimal(_ value: String) -> Decimal? {
let cleaned = value
.replacingOccurrences(of: ",", with: ".")
.trimmingCharacters(in: .whitespaces)
guard !cleaned.isEmpty else { return nil }
return Decimal(string: cleaned)
}
}
#Preview {
AddTransactionView { _, _, _, _, _, _ in }
}
@@ -55,9 +55,6 @@ struct SourceDetailView: View {
metricsSection
}
// Transactions
transactionsSection
// Snapshots List
snapshotsSection
}
@@ -95,18 +92,6 @@ struct SourceDetailView: View {
MissingSnapshotView()
}
}
.sheet(isPresented: $viewModel.showingAddTransaction) {
AddTransactionView { type, date, shares, price, amount, notes in
viewModel.addTransaction(
type: type,
date: date,
shares: shares,
price: price,
amount: amount,
notes: notes
)
}
}
.sheet(isPresented: $viewModel.showingEditSource) {
EditSourceView(source: viewModel.source)
}
@@ -187,30 +172,16 @@ struct SourceDetailView: View {
// MARK: - Quick Actions
private var quickActions: some View {
HStack(spacing: 12) {
Button {
viewModel.showingAddSnapshot = true
} label: {
Label("Add Snapshot", systemImage: "plus.circle.fill")
.font(.subheadline.weight(.semibold))
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary)
.foregroundColor(.white)
.cornerRadius(AppConstants.UI.cornerRadius)
}
Button {
viewModel.showingAddTransaction = true
} label: {
Label("Add Transaction", systemImage: "arrow.left.arrow.right.circle")
.font(.subheadline.weight(.semibold))
.frame(maxWidth: .infinity)
.padding()
.background(Color.appSecondary.opacity(0.15))
.foregroundColor(.appSecondary)
.cornerRadius(AppConstants.UI.cornerRadius)
}
Button {
viewModel.showingAddSnapshot = true
} label: {
Label("Add Snapshot", systemImage: "plus.circle.fill")
.font(.subheadline.weight(.semibold))
.frame(maxWidth: .infinity)
.padding()
.background(Color.appPrimary)
.foregroundColor(.white)
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
@@ -362,56 +333,6 @@ struct SourceDetailView: View {
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
// MARK: - Transactions Section
private var transactionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Transactions")
.font(.headline)
Spacer()
Text("\(viewModel.transactions.count)")
.font(.subheadline)
.foregroundColor(.secondary)
}
if viewModel.transactions.isEmpty {
Text("Add buys, sells, dividends, and fees to track cashflows.")
.font(.subheadline)
.foregroundColor(.secondary)
} else {
HStack(spacing: 12) {
MetricChip(title: "Invested", value: viewModel.source.totalInvested.compactCurrencyString)
MetricChip(title: "Dividends", value: viewModel.source.totalDividends.compactCurrencyString)
MetricChip(title: "Fees", value: viewModel.source.totalFees.compactCurrencyString)
}
ForEach(viewModel.transactions.prefix(5)) { transaction in
TransactionRow(transaction: transaction)
.contextMenu {
Button(role: .destructive) {
viewModel.deleteTransaction(transaction)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
if viewModel.transactions.count > 5 {
Text("+ \(viewModel.transactions.count - 5) more")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
// MARK: - Snapshots Section
private var snapshotsSection: some View {
@@ -567,32 +488,6 @@ struct SnapshotRowView: View {
}
}
// MARK: - Transaction Row View
struct TransactionRow: View {
let transaction: Transaction
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(transaction.transactionType.displayName)
.font(.subheadline.weight(.semibold))
Text(transaction.date.friendlyDescription)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(transaction.decimalAmount.compactCurrencyString)
.font(.subheadline.weight(.semibold))
.foregroundColor(transaction.transactionType == .fee ? .negativeRed : .primary)
}
.padding(.vertical, 4)
}
}
struct MetricChip: View {
let title: String
let value: String