MoneyCounterApp/MoneyCounter/ContentView.swift
2026-04-06 01:25:41 -07:00

234 lines
8.8 KiB
Swift

import SwiftUI
import SwiftData
struct ContentView: View {
@State private var viewModel = MoneyCounterViewModel()
@FocusState private var focusedField: UUID?
@FocusState private var isFloatFocused: Bool
@AppStorage("currentSessionNotes") private var sessionNotes: String = ""
@Environment(\.modelContext) private var modelContext
@State private var showingNotes = false
@State private var showingHistory = false
@State private var showingResetAlert = false
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Menu {
Button(action: { showingNotes = true }) {
Label("Session Notes", systemImage: "note.text")
}
Button(action: { showingHistory = true }) {
Label("Saved History", systemImage: "clock.arrow.circlepath")
}
Divider()
Button(action: saveCurrentSession) {
Label("Save This Total", systemImage: "square.and.arrow.down")
}
} label: {
Image(systemName: "ellipsis.circle")
.font(.title)
.foregroundColor(.white)
}
Spacer()
Text("Money Counter")
.font(.title2).bold()
.foregroundColor(.white)
Spacer()
Button(action: { showingResetAlert = true }) {
Image(systemName: "arrow.counterclockwise")
.font(.title)
.foregroundColor(.white)
}
}
.padding()
.background(Color(red: 0.4, green: 0.7, blue: 0.4))
// Scrolling Denominations
ScrollView {
VStack(spacing: 24) {
ForEach($viewModel.looseDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemGreen))
}
VStack(alignment: .leading, spacing: 8) {
Divider().background(Color.gray)
Text("Rolls")
.font(.title3)
.bold()
.foregroundColor(.secondary)
}
.padding(.top, 8)
ForEach($viewModel.rollDenominations) { $denom in
DenominationRow(denomination: $denom, focusedField: $focusedField, valueColor: Color(UIColor.systemRed))
}
}
.padding()
}
.contentShape(Rectangle())
.onTapGesture { dismissKeyboard() }
// 1. Native drag-to-dismiss behavior
.scrollDismissesKeyboard(.interactively)
// Footer (Math & Totals)
VStack(spacing: 8) {
HStack {
Text("Starting Float")
.font(.headline)
.foregroundColor(.secondary)
Spacer()
HStack(spacing: 2) {
Text("$")
.foregroundColor(.secondary)
TextField("0.00", value: $viewModel.startingFloat, format: .number.precision(.fractionLength(2)))
.keyboardType(.decimalPad)
.focused($isFloatFocused)
.multilineTextAlignment(.trailing)
}
.font(.headline)
.padding(6)
.frame(width: 100)
.background(Color(UIColor.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 8))
}
if viewModel.startingFloat > 0 {
HStack {
Text(viewModel.discrepancy >= 0 ? "Deposit Amount" : "Shortage")
.font(.headline)
Spacer()
Text(String(format: "$%.2f", viewModel.discrepancy))
.font(.headline)
.foregroundColor(viewModel.discrepancy >= 0 ? .primary : .red)
}
}
Divider()
HStack {
Text("Total")
.font(.largeTitle)
.bold()
Spacer()
Text(String(format: "$%.2f", viewModel.total))
.font(.largeTitle)
.bold()
}
}
.padding()
.background(Color(UIColor.secondarySystemBackground))
.foregroundColor(.primary)
}
.background(Color(UIColor.systemBackground))
// 2. Adds the "Done" button directly above the keyboard
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
dismissKeyboard()
}
.font(.headline)
.foregroundColor(.blue)
}
}
.onChange(of: viewModel.looseDenominations) { _, _ in viewModel.saveCache() }
.onChange(of: viewModel.rollDenominations) { _, _ in viewModel.saveCache() }
.onChange(of: viewModel.startingFloat) { _, _ in viewModel.saveCache() }
.sheet(isPresented: $showingNotes) {
NotesView(notes: $sessionNotes, total: viewModel.total, discrepancy: viewModel.discrepancy, expectedFloat: viewModel.startingFloat)
}
.sheet(isPresented: $showingHistory) {
HistoryView(viewModel: viewModel)
}
.confirmationDialog("Reset Everything?", isPresented: $showingResetAlert) {
Button("Reset Counts & Notes", role: .destructive) {
withAnimation { viewModel.reset() }
sessionNotes = ""
}
Button("Reset Counts Only", role: .destructive) {
withAnimation { viewModel.reset() }
}
Button("Cancel", role: .cancel) { }
}
}
private func dismissKeyboard() {
focusedField = nil
isFloatFocused = false
}
private func saveCurrentSession() {
let newSave = SavedCount(
total: viewModel.total,
startingFloat: viewModel.startingFloat,
notes: sessionNotes,
snapshotData: viewModel.generateSnapshot()
)
modelContext.insert(newSave)
}
}
// DenominationRow remains exactly the same as the previous iteration
struct DenominationRow: View {
@Binding var denomination: MoneyCounterViewModel.Denomination
var focusedField: FocusState<UUID?>.Binding
var valueColor: Color
var body: some View {
HStack(spacing: 16) {
HStack {
Text(formatValue(denomination.value))
.font(.title2).bold()
.foregroundColor(valueColor)
.frame(width: 70, alignment: .leading)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture { focusedField.wrappedValue = nil }
Button(action: {
denomination.count += 1
focusedField.wrappedValue = nil
}) {
Image(systemName: "plus.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
.font(.title)
}
Button(action: {
if denomination.count > 0 { denomination.count -= 1 }
focusedField.wrappedValue = nil
}) {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
.font(.title)
}
TextField("0", value: $denomination.count, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.font(.title2).bold()
.frame(width: 80)
.padding(.vertical, 4)
.focused(focusedField, equals: denomination.id)
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(UIColor.separator)),
alignment: .bottom
)
.foregroundColor(.primary)
}
}
private func formatValue(_ value: Double) -> String {
if value >= 1.0 { return String(format: "%.1f", value) }
else { return String(format: "%.2f", value) }
}
}