234 lines
8.8 KiB
Swift
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) }
|
|
}
|
|
}
|