Files
Options-SideKick/ios/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift
olsch01 14d715ed14 Fix 25 Xcode build errors and 2 warnings
- Add `import Combine` to PortfolioViewModel, RecommendationsViewModel,
  PositionsViewModel, AlertsViewModel, and NotificationHandler — required
  for @Published and ObservableObject to resolve correctly in Swift
- Fix @MainActor isolation error in PositionDetailView: replace broken
  default-parameter init (PositionsViewModel() in sync context) with
  @StateObject private var localVM and an optional parentVM parameter
- Update OpenPositionsView call site to use new parentVM: label
- Fix var→let warning in PortfolioViewModel.add()
- Remove unused `old` variable in AppDelegate.registerDevice()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:46:16 -04:00

157 lines
6.5 KiB
Swift

import SwiftUI
struct PositionDetailView: View {
let position: OptionPosition
/// Pass in from parent (OpenPositionsView) so close/roll updates the parent list.
/// If nil, a local vm is used (e.g. when navigating from DashboardView).
var parentVM: PositionsViewModel? = nil
@StateObject private var localVM = PositionsViewModel()
@State private var signals: SignalSnapshot? = nil
@State private var isLoadingSignals = false
@State private var showCloseConfirm = false
@State private var showRollConfirm = false
@Environment(\.dismiss) var dismiss
private var vm: PositionsViewModel { parentVM ?? localVM }
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// Position summary
summarySection
Divider()
// Current signals
if isLoadingSignals {
HStack {
ProgressView()
Text("Loading signals...").font(.caption).foregroundStyle(.secondary)
}
.padding()
} else if let signals {
signalsSection(signals)
Divider()
}
// Actions
if position.status == "open" {
actionSection
}
}
.padding()
}
.navigationTitle("\(position.ticker) \(position.strategyLabel)")
.navigationBarTitleDisplayMode(.inline)
.task { await loadSignals() }
.confirmationDialog("Close Position", isPresented: $showCloseConfirm) {
Button("Bought Back", role: .destructive) {
Task { await vm.close(position: position, reason: "bought_back"); dismiss() }
}
Button("Expired Worthless") {
Task { await vm.close(position: position, reason: "expired"); dismiss() }
}
}
.confirmationDialog("Roll Position", isPresented: $showRollConfirm) {
Button("Mark as Rolled", role: .destructive) {
Task { await vm.roll(position: position); dismiss() }
}
}
}
// Sections
private var summarySection: some View {
VStack(alignment: .leading, spacing: 10) {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
metricCard("Strike", String(format: "$%.2f", position.strike))
metricCard("Expiration", position.expiration)
metricCard("Premium", String(format: "$%.2f", position.premiumReceived))
metricCard("Contracts", "\(position.contracts)")
metricCard("Total Credit", String(format: "$%.2f", position.totalCredit))
if let dte = position.daysToExpiry {
metricCard("Days Left", "\(dte)d", highlight: dte <= 5)
}
}
}
}
private func signalsSection(_ snap: SignalSnapshot) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text("Current Signals")
.font(.headline)
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
metricCard("IV Rank", String(format: "%.0f%%", snap.ivRank), highlight: snap.ivRank >= 50)
metricCard("Trend", snap.trend.capitalized)
if let support = snap.nearestSupport {
metricCard("Support", String(format: "$%.2f", support))
}
if let resistance = snap.nearestResistance {
metricCard("Resistance", String(format: "$%.2f", resistance))
}
}
if let earnings = snap.earningsDate {
HStack(spacing: 6) {
Image(systemName: "calendar.badge.exclamationmark")
.foregroundStyle(Constants.Color.warning)
Text("Earnings: \(earnings)")
.font(.caption)
.foregroundStyle(Constants.Color.warning)
}
}
}
}
private var actionSection: some View {
VStack(spacing: 10) {
Button {
showCloseConfirm = true
} label: {
Label("Close Position", systemImage: "xmark.circle")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(Constants.Color.destructive)
Button {
showRollConfirm = true
} label: {
Label("Roll Position", systemImage: "arrow.2.circlepath")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(Constants.Color.accent)
}
}
// Helpers
private func metricCard(_ label: String, _ value: String, highlight: Bool = false) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(label)
.font(.caption)
.foregroundStyle(.secondary)
Text(value)
.font(.subheadline.weight(.semibold))
.foregroundStyle(highlight ? Constants.Color.strong : .primary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 8))
}
private func loadSignals() async {
isLoadingSignals = true
do {
signals = try await APIClient.shared.request(
.getSignals(position.ticker),
body: Optional<String>.none
)
} catch {
// Non-critical just don't show signals
}
isLoadingSignals = false
}
}