Fix all build errors in Xcode-managed nested source files
The Xcode project was created inside the existing folder structure, producing a second copy of all source files at the deeper nested path that Xcode actually compiles. All fixes are now applied to both paths. Changes: - Add `import Combine` to nested ViewModels (Portfolio, Recommendations, Positions, Alerts) and NotificationHandler — required for @Published - Add `import UIKit` to NotificationPermissions — UIApplication is UIKit - Rewrite APIClient to use `(any Encodable)?` instead of invalid `(some Encodable)?` syntax; add encodeAny() helper to open existential for JSONEncoder; remove private EmptyBody type - Replace all `body: Optional<String>.none` / `Optional<EmptyBody>.none` call sites with plain `nil` across all ViewModels and Views - Sync all fixes between nested Xcode path and outer source path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,6 @@ final class APIClient {
|
|||||||
d.dateDecodingStrategy = .custom { decoder in
|
d.dateDecodingStrategy = .custom { decoder in
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let str = try container.decode(String.self)
|
let str = try container.decode(String.self)
|
||||||
// Try ISO8601 with fractional seconds first, then without
|
|
||||||
let formatters: [ISO8601DateFormatter] = [
|
let formatters: [ISO8601DateFormatter] = [
|
||||||
{
|
{
|
||||||
let f = ISO8601DateFormatter()
|
let f = ISO8601DateFormatter()
|
||||||
@@ -35,11 +34,12 @@ final class APIClient {
|
|||||||
for fmt in formatters {
|
for fmt in formatters {
|
||||||
if let date = fmt.date(from: str) { return date }
|
if let date = fmt.date(from: str) { return date }
|
||||||
}
|
}
|
||||||
// Try plain date (YYYY-MM-DD) for date-only fields decoded as Date
|
|
||||||
let df = DateFormatter()
|
let df = DateFormatter()
|
||||||
df.dateFormat = "yyyy-MM-dd"
|
df.dateFormat = "yyyy-MM-dd"
|
||||||
if let date = df.date(from: str) { return date }
|
if let date = df.date(from: str) { return date }
|
||||||
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Cannot parse date: \(str)"))
|
throw DecodingError.dataCorrupted(
|
||||||
|
.init(codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Cannot parse date: \(str)"))
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}()
|
}()
|
||||||
@@ -54,7 +54,7 @@ final class APIClient {
|
|||||||
|
|
||||||
func request<T: Decodable>(
|
func request<T: Decodable>(
|
||||||
_ endpoint: Endpoint,
|
_ endpoint: Endpoint,
|
||||||
body: (some Encodable)? = Optional<EmptyBody>.none
|
body: (any Encodable)? = nil
|
||||||
) async throws -> T {
|
) async throws -> T {
|
||||||
guard let token = LocalStore.shared.deviceToken else {
|
guard let token = LocalStore.shared.deviceToken else {
|
||||||
throw APIError.noDeviceToken
|
throw APIError.noDeviceToken
|
||||||
@@ -66,7 +66,7 @@ final class APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Version that doesn't return a body (e.g. DELETE 204)
|
/// Version that doesn't return a body (e.g. DELETE 204)
|
||||||
func requestVoid(_ endpoint: Endpoint, body: (some Encodable)? = Optional<EmptyBody>.none) async throws {
|
func requestVoid(_ endpoint: Endpoint, body: (any Encodable)? = nil) async throws {
|
||||||
guard let token = LocalStore.shared.deviceToken else {
|
guard let token = LocalStore.shared.deviceToken else {
|
||||||
throw APIError.noDeviceToken
|
throw APIError.noDeviceToken
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,10 @@ final class APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// For device registration (no token yet)
|
/// For device registration (no token yet)
|
||||||
func requestNoAuth<T: Decodable>(_ endpoint: Endpoint, body: (some Encodable)? = Optional<EmptyBody>.none) async throws -> T {
|
func requestNoAuth<T: Decodable>(
|
||||||
|
_ endpoint: Endpoint,
|
||||||
|
body: (any Encodable)? = nil
|
||||||
|
) async throws -> T {
|
||||||
let req = try buildRequest(endpoint, deviceToken: nil, body: body)
|
let req = try buildRequest(endpoint, deviceToken: nil, body: body)
|
||||||
let (data, response) = try await session.data(for: req)
|
let (data, response) = try await session.data(for: req)
|
||||||
try validateResponse(response, data: data)
|
try validateResponse(response, data: data)
|
||||||
@@ -85,7 +88,11 @@ final class APIClient {
|
|||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private func buildRequest(_ endpoint: Endpoint, deviceToken: String?, body: (some Encodable)?) throws -> URLRequest {
|
private func buildRequest(
|
||||||
|
_ endpoint: Endpoint,
|
||||||
|
deviceToken: String?,
|
||||||
|
body: (any Encodable)?
|
||||||
|
) throws -> URLRequest {
|
||||||
guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else {
|
guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else {
|
||||||
throw APIError.invalidURL
|
throw APIError.invalidURL
|
||||||
}
|
}
|
||||||
@@ -96,11 +103,19 @@ final class APIClient {
|
|||||||
request.setValue(token, forHTTPHeaderField: "X-Device-Token")
|
request.setValue(token, forHTTPHeaderField: "X-Device-Token")
|
||||||
}
|
}
|
||||||
if let body {
|
if let body {
|
||||||
request.httpBody = try encoder.encode(body)
|
request.httpBody = try encodeAny(body)
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens the `any Encodable` existential so JSONEncoder can encode it.
|
||||||
|
private func encodeAny(_ value: any Encodable) throws -> Data {
|
||||||
|
func encode<T: Encodable>(_ v: T) throws -> Data {
|
||||||
|
try encoder.encode(v)
|
||||||
|
}
|
||||||
|
return try encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
private func validateResponse(_ response: URLResponse, data: Data) throws {
|
private func validateResponse(_ response: URLResponse, data: Data) throws {
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
throw APIError.invalidResponse
|
throw APIError.invalidResponse
|
||||||
@@ -121,8 +136,6 @@ final class APIClient {
|
|||||||
|
|
||||||
// ─── Supporting types ──────────────────────────────────────────────────────────
|
// ─── Supporting types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private struct EmptyBody: Encodable {}
|
|
||||||
|
|
||||||
enum APIError: LocalizedError {
|
enum APIError: LocalizedError {
|
||||||
case noDeviceToken
|
case noDeviceToken
|
||||||
case invalidURL
|
case invalidURL
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
Binary file not shown.
@@ -42,9 +42,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
apns_token: token,
|
apns_token: token,
|
||||||
device_name: UIDevice.current.name
|
device_name: UIDevice.current.name
|
||||||
)
|
)
|
||||||
// Temporarily set the token so APIClient has it, but use requestNoAuth
|
// Ensure token is stored before the request so APIClient can use it
|
||||||
// since this is the registration call itself
|
|
||||||
let old = LocalStore.shared.deviceToken
|
|
||||||
LocalStore.shared.deviceToken = token
|
LocalStore.shared.deviceToken = token
|
||||||
let response: DeviceResponse = try await APIClient.shared.requestNoAuth(
|
let response: DeviceResponse = try await APIClient.shared.requestNoAuth(
|
||||||
.registerDevice,
|
.registerDevice,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ final class APIClient {
|
|||||||
d.dateDecodingStrategy = .custom { decoder in
|
d.dateDecodingStrategy = .custom { decoder in
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let str = try container.decode(String.self)
|
let str = try container.decode(String.self)
|
||||||
// Try ISO8601 with fractional seconds first, then without
|
|
||||||
let formatters: [ISO8601DateFormatter] = [
|
let formatters: [ISO8601DateFormatter] = [
|
||||||
{
|
{
|
||||||
let f = ISO8601DateFormatter()
|
let f = ISO8601DateFormatter()
|
||||||
@@ -35,11 +34,12 @@ final class APIClient {
|
|||||||
for fmt in formatters {
|
for fmt in formatters {
|
||||||
if let date = fmt.date(from: str) { return date }
|
if let date = fmt.date(from: str) { return date }
|
||||||
}
|
}
|
||||||
// Try plain date (YYYY-MM-DD) for date-only fields decoded as Date
|
|
||||||
let df = DateFormatter()
|
let df = DateFormatter()
|
||||||
df.dateFormat = "yyyy-MM-dd"
|
df.dateFormat = "yyyy-MM-dd"
|
||||||
if let date = df.date(from: str) { return date }
|
if let date = df.date(from: str) { return date }
|
||||||
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Cannot parse date: \(str)"))
|
throw DecodingError.dataCorrupted(
|
||||||
|
.init(codingPath: decoder.codingPath,
|
||||||
|
debugDescription: "Cannot parse date: \(str)"))
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}()
|
}()
|
||||||
@@ -54,7 +54,7 @@ final class APIClient {
|
|||||||
|
|
||||||
func request<T: Decodable>(
|
func request<T: Decodable>(
|
||||||
_ endpoint: Endpoint,
|
_ endpoint: Endpoint,
|
||||||
body: (some Encodable)? = Optional<EmptyBody>.none
|
body: (any Encodable)? = nil
|
||||||
) async throws -> T {
|
) async throws -> T {
|
||||||
guard let token = LocalStore.shared.deviceToken else {
|
guard let token = LocalStore.shared.deviceToken else {
|
||||||
throw APIError.noDeviceToken
|
throw APIError.noDeviceToken
|
||||||
@@ -66,7 +66,7 @@ final class APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Version that doesn't return a body (e.g. DELETE 204)
|
/// Version that doesn't return a body (e.g. DELETE 204)
|
||||||
func requestVoid(_ endpoint: Endpoint, body: (some Encodable)? = Optional<EmptyBody>.none) async throws {
|
func requestVoid(_ endpoint: Endpoint, body: (any Encodable)? = nil) async throws {
|
||||||
guard let token = LocalStore.shared.deviceToken else {
|
guard let token = LocalStore.shared.deviceToken else {
|
||||||
throw APIError.noDeviceToken
|
throw APIError.noDeviceToken
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,10 @@ final class APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// For device registration (no token yet)
|
/// For device registration (no token yet)
|
||||||
func requestNoAuth<T: Decodable>(_ endpoint: Endpoint, body: (some Encodable)? = Optional<EmptyBody>.none) async throws -> T {
|
func requestNoAuth<T: Decodable>(
|
||||||
|
_ endpoint: Endpoint,
|
||||||
|
body: (any Encodable)? = nil
|
||||||
|
) async throws -> T {
|
||||||
let req = try buildRequest(endpoint, deviceToken: nil, body: body)
|
let req = try buildRequest(endpoint, deviceToken: nil, body: body)
|
||||||
let (data, response) = try await session.data(for: req)
|
let (data, response) = try await session.data(for: req)
|
||||||
try validateResponse(response, data: data)
|
try validateResponse(response, data: data)
|
||||||
@@ -85,7 +88,11 @@ final class APIClient {
|
|||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private func buildRequest(_ endpoint: Endpoint, deviceToken: String?, body: (some Encodable)?) throws -> URLRequest {
|
private func buildRequest(
|
||||||
|
_ endpoint: Endpoint,
|
||||||
|
deviceToken: String?,
|
||||||
|
body: (any Encodable)?
|
||||||
|
) throws -> URLRequest {
|
||||||
guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else {
|
guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else {
|
||||||
throw APIError.invalidURL
|
throw APIError.invalidURL
|
||||||
}
|
}
|
||||||
@@ -96,11 +103,19 @@ final class APIClient {
|
|||||||
request.setValue(token, forHTTPHeaderField: "X-Device-Token")
|
request.setValue(token, forHTTPHeaderField: "X-Device-Token")
|
||||||
}
|
}
|
||||||
if let body {
|
if let body {
|
||||||
request.httpBody = try encoder.encode(body)
|
request.httpBody = try encodeAny(body)
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens the `any Encodable` existential so JSONEncoder can encode it.
|
||||||
|
private func encodeAny(_ value: any Encodable) throws -> Data {
|
||||||
|
func encode<T: Encodable>(_ v: T) throws -> Data {
|
||||||
|
try encoder.encode(v)
|
||||||
|
}
|
||||||
|
return try encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
private func validateResponse(_ response: URLResponse, data: Data) throws {
|
private func validateResponse(_ response: URLResponse, data: Data) throws {
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
throw APIError.invalidResponse
|
throw APIError.invalidResponse
|
||||||
@@ -121,8 +136,6 @@ final class APIClient {
|
|||||||
|
|
||||||
// ─── Supporting types ──────────────────────────────────────────────────────────
|
// ─── Supporting types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private struct EmptyBody: Encodable {}
|
|
||||||
|
|
||||||
enum APIError: LocalizedError {
|
enum APIError: LocalizedError {
|
||||||
case noDeviceToken
|
case noDeviceToken
|
||||||
case invalidURL
|
case invalidURL
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
/// Handles incoming push notifications — both foreground and background tap.
|
/// Handles incoming push notifications — both foreground and background tap.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AlertsViewModel: ObservableObject {
|
final class AlertsViewModel: ObservableObject {
|
||||||
@@ -14,7 +15,7 @@ final class AlertsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
alerts = try await APIClient.shared.request(
|
alerts = try await APIClient.shared.request(
|
||||||
.getAlerts(unreadOnly: unreadOnly),
|
.getAlerts(unreadOnly: unreadOnly),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
LocalStore.shared.unreadAlertCount = unreadCount
|
LocalStore.shared.unreadAlertCount = unreadCount
|
||||||
} catch {
|
} catch {
|
||||||
@@ -27,7 +28,7 @@ final class AlertsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
let updated: AppAlert = try await APIClient.shared.request(
|
let updated: AppAlert = try await APIClient.shared.request(
|
||||||
.acknowledgeAlert(alert.id),
|
.acknowledgeAlert(alert.id),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
if let idx = alerts.firstIndex(where: { $0.id == alert.id }) {
|
if let idx = alerts.firstIndex(where: { $0.id == alert.id }) {
|
||||||
alerts[idx] = updated
|
alerts[idx] = updated
|
||||||
|
|||||||
@@ -45,18 +45,18 @@ final class DashboardViewModel: ObservableObject {
|
|||||||
// ─── Private loaders ──────────────────────────────────────────────────────
|
// ─── Private loaders ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private func loadStocks() async -> [PortfolioPosition] {
|
private func loadStocks() async -> [PortfolioPosition] {
|
||||||
(try? await APIClient.shared.request(.getPortfolio, body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getPortfolio, body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadOptions() async -> [OptionPosition] {
|
private func loadOptions() async -> [OptionPosition] {
|
||||||
(try? await APIClient.shared.request(.getPositions(status: "open"), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getPositions(status: "open"), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadRecommendations() async -> [Recommendation] {
|
private func loadRecommendations() async -> [Recommendation] {
|
||||||
(try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadAlerts() async -> [AppAlert] {
|
private func loadAlerts() async -> [AppAlert] {
|
||||||
(try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class PortfolioViewModel: ObservableObject {
|
final class PortfolioViewModel: ObservableObject {
|
||||||
@@ -10,7 +11,7 @@ final class PortfolioViewModel: ObservableObject {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
do {
|
do {
|
||||||
positions = try await APIClient.shared.request(.getPortfolio, body: Optional<String>.none)
|
positions = try await APIClient.shared.request(.getPortfolio, body: nil)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ final class PortfolioViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func add(ticker: String, shares: Int, costBasis: Double?) async {
|
func add(ticker: String, shares: Int, costBasis: Double?) async {
|
||||||
var updated = positions
|
let updated = positions
|
||||||
struct AddBody: Encodable {
|
struct AddBody: Encodable {
|
||||||
let ticker: String
|
let ticker: String
|
||||||
let shares: Int
|
let shares: Int
|
||||||
@@ -57,7 +58,7 @@ final class PortfolioViewModel: ObservableObject {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
do {
|
do {
|
||||||
try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: Optional<String>.none)
|
try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: nil)
|
||||||
positions.removeAll { $0.ticker == ticker }
|
positions.removeAll { $0.ticker == ticker }
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class PositionsViewModel: ObservableObject {
|
final class PositionsViewModel: ObservableObject {
|
||||||
@@ -15,7 +16,7 @@ final class PositionsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
positions = try await APIClient.shared.request(
|
positions = try await APIClient.shared.request(
|
||||||
.getPositions(status: nil),
|
.getPositions(status: nil),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class RecommendationsViewModel: ObservableObject {
|
final class RecommendationsViewModel: ObservableObject {
|
||||||
@@ -21,7 +22,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
recommendations = try await APIClient.shared.request(
|
recommendations = try await APIClient.shared.request(
|
||||||
.getRecommendations(timeHorizon: nil),
|
.getRecommendations(timeHorizon: nil),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
@@ -35,7 +36,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
recommendations = try await APIClient.shared.request(
|
recommendations = try await APIClient.shared.request(
|
||||||
.refreshRecommendations,
|
.refreshRecommendations,
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
@@ -47,7 +48,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
return try await APIClient.shared.request(
|
return try await APIClient.shared.request(
|
||||||
.getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon),
|
.getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct OpenPositionsView: View {
|
|||||||
if !vm.openPositions.isEmpty {
|
if !vm.openPositions.isEmpty {
|
||||||
Section("Open") {
|
Section("Open") {
|
||||||
ForEach(vm.openPositions) { position in
|
ForEach(vm.openPositions) { position in
|
||||||
NavigationLink(destination: PositionDetailView(position: position, vm: vm)) {
|
NavigationLink(destination: PositionDetailView(position: position, parentVM: vm)) {
|
||||||
LoggedPositionRow(position: position)
|
LoggedPositionRow(position: position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import SwiftUI
|
|||||||
|
|
||||||
struct PositionDetailView: View {
|
struct PositionDetailView: View {
|
||||||
let position: OptionPosition
|
let position: OptionPosition
|
||||||
@ObservedObject var vm: PositionsViewModel
|
/// 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 signals: SignalSnapshot? = nil
|
||||||
@State private var isLoadingSignals = false
|
@State private var isLoadingSignals = false
|
||||||
@State private var showCloseConfirm = false
|
@State private var showCloseConfirm = false
|
||||||
@State private var showRollConfirm = false
|
@State private var showRollConfirm = false
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
init(position: OptionPosition, vm: PositionsViewModel = PositionsViewModel()) {
|
private var vm: PositionsViewModel { parentVM ?? localVM }
|
||||||
self.position = position
|
|
||||||
self.vm = vm
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -146,7 +146,7 @@ struct PositionDetailView: View {
|
|||||||
do {
|
do {
|
||||||
signals = try await APIClient.shared.request(
|
signals = try await APIClient.shared.request(
|
||||||
.getSignals(position.ticker),
|
.getSignals(position.ticker),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// Non-critical — just don't show signals
|
// Non-critical — just don't show signals
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class AlertsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
alerts = try await APIClient.shared.request(
|
alerts = try await APIClient.shared.request(
|
||||||
.getAlerts(unreadOnly: unreadOnly),
|
.getAlerts(unreadOnly: unreadOnly),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
LocalStore.shared.unreadAlertCount = unreadCount
|
LocalStore.shared.unreadAlertCount = unreadCount
|
||||||
} catch {
|
} catch {
|
||||||
@@ -28,7 +28,7 @@ final class AlertsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
let updated: AppAlert = try await APIClient.shared.request(
|
let updated: AppAlert = try await APIClient.shared.request(
|
||||||
.acknowledgeAlert(alert.id),
|
.acknowledgeAlert(alert.id),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
if let idx = alerts.firstIndex(where: { $0.id == alert.id }) {
|
if let idx = alerts.firstIndex(where: { $0.id == alert.id }) {
|
||||||
alerts[idx] = updated
|
alerts[idx] = updated
|
||||||
|
|||||||
@@ -45,18 +45,18 @@ final class DashboardViewModel: ObservableObject {
|
|||||||
// ─── Private loaders ──────────────────────────────────────────────────────
|
// ─── Private loaders ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private func loadStocks() async -> [PortfolioPosition] {
|
private func loadStocks() async -> [PortfolioPosition] {
|
||||||
(try? await APIClient.shared.request(.getPortfolio, body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getPortfolio, body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadOptions() async -> [OptionPosition] {
|
private func loadOptions() async -> [OptionPosition] {
|
||||||
(try? await APIClient.shared.request(.getPositions(status: "open"), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getPositions(status: "open"), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadRecommendations() async -> [Recommendation] {
|
private func loadRecommendations() async -> [Recommendation] {
|
||||||
(try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadAlerts() async -> [AppAlert] {
|
private func loadAlerts() async -> [AppAlert] {
|
||||||
(try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: Optional<String>.none)) ?? []
|
(try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: nil)) ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ final class PortfolioViewModel: ObservableObject {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
do {
|
do {
|
||||||
positions = try await APIClient.shared.request(.getPortfolio, body: Optional<String>.none)
|
positions = try await APIClient.shared.request(.getPortfolio, body: nil)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ final class PortfolioViewModel: ObservableObject {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
do {
|
do {
|
||||||
try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: Optional<String>.none)
|
try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: nil)
|
||||||
positions.removeAll { $0.ticker == ticker }
|
positions.removeAll { $0.ticker == ticker }
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ final class PositionsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
positions = try await APIClient.shared.request(
|
positions = try await APIClient.shared.request(
|
||||||
.getPositions(status: nil),
|
.getPositions(status: nil),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
recommendations = try await APIClient.shared.request(
|
recommendations = try await APIClient.shared.request(
|
||||||
.getRecommendations(timeHorizon: nil),
|
.getRecommendations(timeHorizon: nil),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
@@ -36,7 +36,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
recommendations = try await APIClient.shared.request(
|
recommendations = try await APIClient.shared.request(
|
||||||
.refreshRecommendations,
|
.refreshRecommendations,
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
@@ -48,7 +48,7 @@ final class RecommendationsViewModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
return try await APIClient.shared.request(
|
return try await APIClient.shared.request(
|
||||||
.getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon),
|
.getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ struct PositionDetailView: View {
|
|||||||
do {
|
do {
|
||||||
signals = try await APIClient.shared.request(
|
signals = try await APIClient.shared.request(
|
||||||
.getSignals(position.ticker),
|
.getSignals(position.ticker),
|
||||||
body: Optional<String>.none
|
body: nil
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// Non-critical — just don't show signals
|
// Non-critical — just don't show signals
|
||||||
|
|||||||
Reference in New Issue
Block a user