Add document viewing, editing, and image deletion features

- Add DocumentDetailScreen and EditDocumentScreen for Compose (Android/Web)
- Add DocumentDetailView and EditDocumentView for iOS SwiftUI
- Add DocumentViewModelWrapper for iOS state management
- Implement document image deletion API integration
- Fix iOS navigation issues with edit button using hidden NavigationLink
- Add clickable warranties in iOS with NavigationLink
- Fix iOS build errors with proper type checking and state handling
- Add support for viewing and managing warranty-specific fields

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-11 14:00:01 -06:00
parent e716c919f3
commit 611f7d853b
12 changed files with 2656 additions and 3 deletions

View File

@@ -0,0 +1,300 @@
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()
private let documentApi = DocumentApi(client: ApiClient_iosKt.createHttpClient())
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
) {
guard let token = TokenStorage.shared.getToken() else {
DispatchQueue.main.async {
self.documentsState = DocumentStateError(message: "Not authenticated")
}
return
}
DispatchQueue.main.async {
self.documentsState = DocumentStateLoading()
}
Task {
do {
let result = try await documentApi.getDocuments(
token: token,
residenceId: residenceId != nil ? KotlinInt(integerLiteral: Int(residenceId!)) : nil,
documentType: documentType,
category: category,
contractorId: contractorId != nil ? KotlinInt(integerLiteral: Int(contractorId!)) : nil,
isActive: isActive != nil ? KotlinBoolean(bool: isActive!) : nil,
expiringSoon: expiringSoon != nil ? KotlinInt(integerLiteral: Int(expiringSoon!)) : nil,
tags: tags,
search: search
)
await MainActor.run {
if let success = result as? ApiResultSuccess<DocumentListResponse> {
let documents = success.data?.results 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) {
guard let token = TokenStorage.shared.getToken() else {
DispatchQueue.main.async {
self.documentDetailState = DocumentDetailStateError(message: "Not authenticated")
}
return
}
DispatchQueue.main.async {
self.documentDetailState = DocumentDetailStateLoading()
}
Task {
do {
let result = try await documentApi.getDocument(token: token, id: id)
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
) {
guard let token = TokenStorage.shared.getToken() else {
DispatchQueue.main.async {
self.updateState = UpdateStateError(message: "Not authenticated")
}
return
}
DispatchQueue.main.async {
self.updateState = UpdateStateLoading()
}
Task {
do {
let result = try await documentApi.updateDocument(
token: token,
id: id,
title: title,
documentType: documentType,
description: description,
category: category,
tags: tags,
notes: notes,
contractorId: nil,
isActive: KotlinBoolean(bool: isActive),
itemName: itemName,
modelNumber: modelNumber,
serialNumber: serialNumber,
provider: provider,
providerContact: providerContact,
claimPhone: claimPhone,
claimEmail: claimEmail,
claimWebsite: claimWebsite,
purchaseDate: purchaseDate,
startDate: startDate,
endDate: endDate,
fileBytes: nil,
fileName: nil,
mimeType: nil
)
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) {
guard let token = TokenStorage.shared.getToken() else {
DispatchQueue.main.async {
self.deleteState = DeleteStateError(message: "Not authenticated")
}
return
}
DispatchQueue.main.async {
self.deleteState = DeleteStateLoading()
}
Task {
do {
let result = try await documentApi.deleteDocument(token: token, 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) {
guard let token = TokenStorage.shared.getToken() else {
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateError(message: "Not authenticated")
}
return
}
DispatchQueue.main.async {
self.deleteImageState = DeleteImageStateLoading()
}
Task {
do {
let result = try await documentApi.deleteDocumentImage(token: token, 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()
}
}
}