Files
honeyDueKMP/iosApp/iosApp/Documents/DocumentViewModelWrapper.swift
Trey t 63a54434ed Add 1-hour cache timeout and fix pull-to-refresh across iOS
- Add configurable cache timeout (CACHE_TIMEOUT_MS) to DataManager
- Fix cache to work with empty results (contractors, documents, residences)
- Change Documents/Warranties view to use client-side filtering for cache efficiency
- Add pull-to-refresh support for empty state views in ListAsyncContentView
- Fix ContractorsListView to pass forceRefresh parameter correctly
- Fix TaskViewModel loading spinner not stopping after refresh completes
- Remove duplicate cache checks in iOS ViewModels, delegate to Kotlin APILayer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 09:50:57 -06:00

260 lines
8.9 KiB
Swift

import Foundation
import ComposeApp
import SwiftUI
// State wrappers for SwiftUI
protocol DocumentState {}
struct DocumentStateIdle: DocumentState {}
struct DocumentStateLoading: DocumentState {}
struct DocumentStateSuccess: DocumentState {
let documents: [Document]
}
struct DocumentStateError: DocumentState {
let message: String
}
protocol DocumentDetailState {}
struct DocumentDetailStateIdle: DocumentDetailState {}
struct DocumentDetailStateLoading: DocumentDetailState {}
struct DocumentDetailStateSuccess: DocumentDetailState {
let document: Document
}
struct DocumentDetailStateError: DocumentDetailState {
let message: String
}
protocol UpdateState {}
struct UpdateStateIdle: UpdateState {}
struct UpdateStateLoading: UpdateState {}
struct UpdateStateSuccess: UpdateState {
let document: Document
}
struct UpdateStateError: UpdateState {
let message: String
}
protocol DeleteState {}
struct DeleteStateIdle: DeleteState {}
struct DeleteStateLoading: DeleteState {}
struct DeleteStateSuccess: DeleteState {}
struct DeleteStateError: DeleteState {
let message: String
}
protocol DeleteImageState {}
struct DeleteImageStateIdle: DeleteImageState {}
struct DeleteImageStateLoading: DeleteImageState {}
struct DeleteImageStateSuccess: DeleteImageState {}
struct DeleteImageStateError: DeleteImageState {
let message: String
}
class DocumentViewModelWrapper: ObservableObject {
@Published var documentsState: DocumentState = DocumentStateIdle()
@Published var documentDetailState: DocumentDetailState = DocumentDetailStateIdle()
@Published var updateState: UpdateState = UpdateStateIdle()
@Published var deleteState: DeleteState = DeleteStateIdle()
@Published var deleteImageState: DeleteImageState = DeleteImageStateIdle()
func loadDocuments(
residenceId: Int32? = nil,
documentType: String? = nil,
category: String? = nil,
contractorId: Int32? = nil,
isActive: Bool? = nil,
expiringSoon: Int32? = nil,
tags: String? = nil,
search: String? = nil
) {
DispatchQueue.main.async {
self.documentsState = DocumentStateLoading()
}
Task {
do {
let result = try await APILayer.shared.getDocuments(
residenceId: residenceId != nil ? KotlinInt(int: residenceId!) : nil,
documentType: documentType,
category: category,
contractorId: contractorId != nil ? KotlinInt(int: contractorId!) : nil,
isActive: isActive != nil ? KotlinBoolean(bool: isActive!) : nil,
expiringSoon: expiringSoon != nil ? KotlinInt(int: expiringSoon!) : nil,
tags: tags,
search: search,
forceRefresh: false
)
await MainActor.run {
if let success = result as? ApiResultSuccess<NSArray> {
let documents = success.data as? [Document] ?? []
self.documentsState = DocumentStateSuccess(documents: documents)
} else if let error = result as? ApiResultError {
self.documentsState = DocumentStateError(message: error.message)
}
}
} catch {
await MainActor.run {
self.documentsState = DocumentStateError(message: error.localizedDescription)
}
}
}
}
func loadDocumentDetail(id: Int32) {
DispatchQueue.main.async {
self.documentDetailState = DocumentDetailStateLoading()
}
Task {
do {
let result = try await APILayer.shared.getDocument(id: id, forceRefresh: false)
await MainActor.run {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.documentDetailState = DocumentDetailStateSuccess(document: document)
} else if let error = result as? ApiResultError {
self.documentDetailState = DocumentDetailStateError(message: error.message)
}
}
} catch {
await MainActor.run {
self.documentDetailState = DocumentDetailStateError(message: error.localizedDescription)
}
}
}
}
func updateDocument(
id: Int32,
title: String,
documentType: String,
description: String? = nil,
category: String? = nil,
tags: String? = nil,
notes: String? = nil,
isActive: Bool = true,
itemName: String? = nil,
modelNumber: String? = nil,
serialNumber: String? = nil,
provider: String? = nil,
providerContact: String? = nil,
claimPhone: String? = nil,
claimEmail: String? = nil,
claimWebsite: String? = nil,
purchaseDate: String? = nil,
startDate: String? = nil,
endDate: String? = nil
) {
DispatchQueue.main.async {
self.updateState = UpdateStateLoading()
}
Task {
do {
let result = try await APILayer.shared.updateDocument(
id: id,
title: title,
documentType: documentType,
description: description,
category: category,
tags: tags,
notes: notes,
contractorId: nil,
isActive: isActive,
itemName: itemName,
modelNumber: modelNumber,
serialNumber: serialNumber,
provider: provider,
providerContact: providerContact,
claimPhone: claimPhone,
claimEmail: claimEmail,
claimWebsite: claimWebsite,
purchaseDate: purchaseDate,
startDate: startDate,
endDate: endDate
)
await MainActor.run {
if let success = result as? ApiResultSuccess<Document>, let document = success.data {
self.updateState = UpdateStateSuccess(document: document)
// Also refresh the detail state
self.documentDetailState = DocumentDetailStateSuccess(document: document)
} else if let error = result as? ApiResultError {
self.updateState = UpdateStateError(message: error.message)
}
}
} catch {
await MainActor.run {
self.updateState = UpdateStateError(message: error.localizedDescription)
}
}
}
}
func deleteDocument(id: Int32) {
DispatchQueue.main.async {
self.deleteState = DeleteStateLoading()
}
Task {
do {
let result = try await APILayer.shared.deleteDocument(id: id)
await MainActor.run {
if result is ApiResultSuccess<KotlinUnit> {
self.deleteState = DeleteStateSuccess()
} else if let error = result as? ApiResultError {
self.deleteState = DeleteStateError(message: error.message)
}
}
} catch {
await MainActor.run {
self.deleteState = DeleteStateError(message: error.localizedDescription)
}
}
}
}
func resetUpdateState() {
DispatchQueue.main.async {
self.updateState = UpdateStateIdle()
}
}
func resetDeleteState() {
DispatchQueue.main.async {
self.deleteState = DeleteStateIdle()
}
}
func deleteDocumentImage(imageId: Int32) {
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateLoading()
}
Task {
do {
let result = try await APILayer.shared.deleteDocumentImage(imageId: imageId)
await MainActor.run {
if result is ApiResultSuccess<KotlinUnit> {
self.deleteImageState = DeleteImageStateSuccess()
} else if let error = result as? ApiResultError {
self.deleteImageState = DeleteImageStateError(message: error.message)
}
}
} catch {
await MainActor.run {
self.deleteImageState = DeleteImageStateError(message: error.localizedDescription)
}
}
}
}
func resetDeleteImageState() {
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateIdle()
}
}
}