Update version

This commit is contained in:
2026-01-16 11:28:26 +01:00
parent 7988257399
commit c6be398e5a
28 changed files with 1061 additions and 193 deletions
@@ -10,6 +10,7 @@ struct AccountEditorView: View {
@State private var inputMode: InputMode = .simple
@State private var notificationFrequency: NotificationFrequency = .monthly
@State private var customFrequencyMonths = 1
@State private var errorMessage: String?
private let accountRepository = AccountRepository()
@@ -25,6 +26,11 @@ struct AccountEditorView: View {
}
} header: {
Text("Account")
} footer: {
if let errorMessage {
Text(errorMessage)
.foregroundColor(.negativeRed)
}
}
Section {
@@ -39,25 +45,6 @@ struct AccountEditorView: View {
Text(inputMode.description)
}
Section {
Picker("Reminder Frequency", selection: $notificationFrequency) {
ForEach(NotificationFrequency.allCases) { frequency in
Text(frequency.displayName).tag(frequency)
}
}
if notificationFrequency == .custom {
Stepper(
"Every \(customFrequencyMonths) month\(customFrequencyMonths > 1 ? "s" : "")",
value: $customFrequencyMonths,
in: 1...24
)
}
} header: {
Text("Account Reminders")
} footer: {
Text("Reminders apply to the whole account.")
}
}
.navigationTitle(account == nil ? "New Account" : "Edit Account")
.navigationBarTitleDisplayMode(.inline)
@@ -73,12 +60,35 @@ struct AccountEditorView: View {
.onAppear {
loadAccount()
}
.onChange(of: name) { _, _ in
validateName()
}
}
.presentationDetents([.large])
}
private var isValid: Bool {
!name.trimmingCharacters(in: .whitespaces).isEmpty
let trimmed = name.trimmingCharacters(in: .whitespaces)
return !trimmed.isEmpty && !isDuplicateName(trimmed)
}
private func validateName() {
let trimmed = name.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty {
errorMessage = nil
return
}
errorMessage = isDuplicateName(trimmed) ? "An account with this name already exists." : nil
}
private func isDuplicateName(_ trimmed: String) -> Bool {
let normalized = trimmed.lowercased()
return accountRepository.accounts.contains { existing in
if let account, existing.id == account.id {
return false
}
return (existing.name).trimmingCharacters(in: .whitespaces).lowercased() == normalized
}
}
private func loadAccount() {
@@ -86,27 +96,33 @@ struct AccountEditorView: View {
name = account.name
currencyCode = account.currencyCode ?? currencyCode
inputMode = InputMode(rawValue: account.inputMode) ?? .simple
notificationFrequency = account.frequency
customFrequencyMonths = Int(account.customFrequencyMonths)
notificationFrequency = .monthly
customFrequencyMonths = 1
validateName()
}
private func saveAccount() {
validateName()
guard errorMessage == nil else { return }
let trimmedName = name.trimmingCharacters(in: .whitespaces)
let enforcedFrequency: NotificationFrequency = .monthly
let enforcedCustomMonths = 1
if let account {
accountRepository.updateAccount(
account,
name: name.trimmingCharacters(in: .whitespaces),
name: trimmedName,
currency: currencyCode,
inputMode: inputMode,
notificationFrequency: notificationFrequency,
customFrequencyMonths: customFrequencyMonths
notificationFrequency: enforcedFrequency,
customFrequencyMonths: enforcedCustomMonths
)
} else {
_ = accountRepository.createAccount(
name: name.trimmingCharacters(in: .whitespaces),
name: trimmedName,
currency: currencyCode,
inputMode: inputMode,
notificationFrequency: notificationFrequency,
customFrequencyMonths: customFrequencyMonths
notificationFrequency: enforcedFrequency,
customFrequencyMonths: enforcedCustomMonths
)
}
@@ -7,6 +7,7 @@ struct AccountsView: View {
@State private var showingAddAccount = false
@State private var selectedAccount: Account?
@State private var showingPaywall = false
@State private var accountToDelete: Account?
var body: some View {
ZStack {
@@ -33,6 +34,13 @@ struct AccountsView: View {
}
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
accountToDelete = account
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} header: {
Text("Accounts")
@@ -43,6 +51,25 @@ struct AccountsView: View {
.scrollContentBackground(.hidden)
}
.navigationTitle("Accounts")
.confirmationDialog(
"Delete Account",
isPresented: Binding(
get: { accountToDelete != nil },
set: { if !$0 { accountToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
guard let accountToDelete else { return }
if accountStore.selectedAccount?.id == accountToDelete.id {
accountStore.selectAllAccounts()
}
accountRepository.deleteAccount(accountToDelete)
self.accountToDelete = nil
}
} message: {
Text("This will remove the account and unlink its sources.")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {