New framework: - AccessibilityLabels.swift: centralized A11y struct with VoiceOver strings - AccessibilityModifiers.swift: reusable .a11yHeader, .a11yDecorative, .a11yButton, .a11yCard, .a11yStatValue View extensions Shared components: decorative elements hidden, stat views combined, status/priority badges labeled, error views announced, empty states grouped Cards: ResidenceCard, TaskCard, DynamicTaskCard, ContractorCard, DocumentCard, WarrantyCard — all grouped with combined labels, chevrons hidden, action buttons labeled Main screens: Login, Register, Residences, Tasks, Contractors, Documents — toolbar buttons labeled, section headers marked, form field hints added Onboarding: all 10 views — header traits, button hints, task selection state, progress indicator, decorative backgrounds hidden Profile/Subscription: toggle hints, theme selection state, feature comparison table accessibility, subscription button labels iOS build verified: BUILD SUCCEEDED
420 lines
15 KiB
Swift
420 lines
15 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
enum DocumentWarrantyTab {
|
|
case warranties
|
|
case documents
|
|
}
|
|
|
|
struct DocumentsWarrantiesView: View {
|
|
@StateObject private var documentViewModel = DocumentViewModel()
|
|
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
|
@State private var selectedTab: DocumentWarrantyTab = .warranties
|
|
@State private var searchText = ""
|
|
@State private var selectedCategory: String? = nil
|
|
@State private var selectedDocType: String? = nil
|
|
@State private var showActiveOnly = true
|
|
@State private var showFilterMenu = false
|
|
@State private var showAddSheet = false
|
|
@State private var showingUpgradePrompt = false
|
|
@State private var pushTargetDocumentId: Int32?
|
|
@State private var navigateToPushDocument = false
|
|
|
|
let residenceId: Int32?
|
|
|
|
var warranties: [Document] {
|
|
documentViewModel.documents.filter { doc in
|
|
guard doc.documentType == "warranty" else { return false }
|
|
if showActiveOnly && doc.isActive != true {
|
|
return false
|
|
}
|
|
if let category = selectedCategory, doc.category?.lowercased() != category.lowercased() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
var documents: [Document] {
|
|
documentViewModel.documents.filter { doc in
|
|
guard doc.documentType != "warranty" else { return false }
|
|
if let docType = selectedDocType, doc.documentType.lowercased() != docType.lowercased() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
WarmGradientBackground()
|
|
|
|
VStack(spacing: 0) {
|
|
// Segmented Control
|
|
OrganicSegmentedControl(selection: $selectedTab)
|
|
.padding(.horizontal, 16)
|
|
.padding(.top, 8)
|
|
|
|
// Search Bar
|
|
OrganicDocSearchBar(text: $searchText, placeholder: L10n.Documents.searchPlaceholder)
|
|
.padding(.horizontal, 16)
|
|
.padding(.top, 8)
|
|
|
|
// Active Filters
|
|
if selectedCategory != nil || selectedDocType != nil || (selectedTab == .warranties && showActiveOnly) {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
if selectedTab == .warranties && showActiveOnly {
|
|
OrganicDocFilterChip(
|
|
title: L10n.Documents.activeOnly,
|
|
icon: "checkmark.circle.fill",
|
|
onRemove: { showActiveOnly = false }
|
|
)
|
|
}
|
|
|
|
if let category = selectedCategory, selectedTab == .warranties {
|
|
OrganicDocFilterChip(
|
|
title: category,
|
|
onRemove: { selectedCategory = nil }
|
|
)
|
|
}
|
|
|
|
if let docType = selectedDocType, selectedTab == .documents {
|
|
OrganicDocFilterChip(
|
|
title: docType,
|
|
onRemove: { selectedDocType = nil }
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
// Content
|
|
if selectedTab == .warranties {
|
|
WarrantiesTabContent(
|
|
viewModel: documentViewModel,
|
|
searchText: searchText
|
|
)
|
|
} else {
|
|
DocumentsTabContent(
|
|
viewModel: documentViewModel,
|
|
searchText: searchText
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
HStack(spacing: 12) {
|
|
// Active Filter (for warranties)
|
|
if selectedTab == .warranties {
|
|
Button(action: {
|
|
showActiveOnly.toggle()
|
|
}) {
|
|
Image(systemName: showActiveOnly ? "checkmark.circle.fill" : "checkmark.circle")
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundColor(showActiveOnly ? Color.appPrimary : Color.appTextSecondary)
|
|
}
|
|
.accessibilityLabel(showActiveOnly ? "Show all warranties" : "Show active warranties only")
|
|
}
|
|
|
|
// Filter Menu
|
|
Menu {
|
|
if selectedTab == .warranties {
|
|
Button(action: {
|
|
selectedCategory = nil
|
|
}) {
|
|
Label(L10n.Documents.allCategories, systemImage: selectedCategory == nil ? "checkmark" : "")
|
|
}
|
|
|
|
Divider()
|
|
|
|
ForEach(DocumentCategory.allCases, id: \.self) { category in
|
|
Button(action: {
|
|
selectedCategory = category.displayName
|
|
}) {
|
|
Label(category.displayName, systemImage: selectedCategory == category.displayName ? "checkmark" : "")
|
|
}
|
|
}
|
|
} else {
|
|
Button(action: {
|
|
selectedDocType = nil
|
|
}) {
|
|
Label(L10n.Documents.allTypes, systemImage: selectedDocType == nil ? "checkmark" : "")
|
|
}
|
|
|
|
Divider()
|
|
|
|
ForEach(DocumentType.allCases, id: \.self) { type in
|
|
Button(action: {
|
|
selectedDocType = type.displayName
|
|
}) {
|
|
Label(type.displayName, systemImage: selectedDocType == type.displayName ? "checkmark" : "")
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
Image(systemName: "line.3.horizontal.decrease.circle")
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary)
|
|
}
|
|
.accessibilityLabel("Filter documents")
|
|
|
|
// Add Button
|
|
Button(action: {
|
|
let currentCount = documentViewModel.documents.count
|
|
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "documents") {
|
|
AnalyticsManager.shared.track(.documentsPaywallShown(currentCount: currentCount))
|
|
showingUpgradePrompt = true
|
|
} else {
|
|
showAddSheet = true
|
|
}
|
|
}) {
|
|
OrganicDocToolbarButton()
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Document.addButton)
|
|
.accessibilityLabel("Add document")
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
AnalyticsManager.shared.trackScreen(.documents)
|
|
if let pendingDocumentId = PushNotificationManager.shared.pendingNavigationDocumentId {
|
|
navigateToDocumentFromPush(documentId: pendingDocumentId)
|
|
}
|
|
loadAllDocuments()
|
|
}
|
|
.sheet(isPresented: $showAddSheet, onDismiss: {
|
|
documentViewModel.loadDocuments(forceRefresh: true)
|
|
}) {
|
|
AddDocumentView(
|
|
residenceId: residenceId,
|
|
initialDocumentType: selectedTab == .warranties ? "warranty" : "general",
|
|
isPresented: $showAddSheet,
|
|
documentViewModel: documentViewModel
|
|
)
|
|
}
|
|
.sheet(isPresented: $showingUpgradePrompt) {
|
|
UpgradePromptView(triggerKey: "view_documents", isPresented: $showingUpgradePrompt)
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: .navigateToDocument)) { notification in
|
|
if let documentId = notification.userInfo?["documentId"] as? Int {
|
|
navigateToDocumentFromPush(documentId: documentId)
|
|
} else {
|
|
selectedTab = .warranties
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $navigateToPushDocument) {
|
|
if let documentId = pushTargetDocumentId {
|
|
DocumentDetailView(documentId: documentId)
|
|
} else {
|
|
Color.clear.onAppear { navigateToPushDocument = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadAllDocuments(forceRefresh: Bool = false) {
|
|
documentViewModel.loadDocuments(forceRefresh: forceRefresh)
|
|
}
|
|
|
|
private func loadWarranties() {
|
|
loadAllDocuments()
|
|
}
|
|
|
|
private func loadDocuments() {
|
|
loadAllDocuments()
|
|
}
|
|
|
|
private func navigateToDocumentFromPush(documentId: Int) {
|
|
// Look up the document to determine the correct tab
|
|
if let document = documentViewModel.documents.first(where: { $0.id?.int32Value == Int32(documentId) }) {
|
|
selectedTab = document.documentType == "warranty" ? .warranties : .documents
|
|
} else {
|
|
// Default to warranties if document not found in cache
|
|
selectedTab = .warranties
|
|
}
|
|
pushTargetDocumentId = Int32(documentId)
|
|
navigateToPushDocument = true
|
|
PushNotificationManager.shared.pendingNavigationDocumentId = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Organic Segmented Control
|
|
|
|
private struct OrganicSegmentedControl: View {
|
|
@Binding var selection: DocumentWarrantyTab
|
|
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
OrganicSegmentButton(
|
|
title: L10n.Documents.warranties,
|
|
icon: "checkmark.shield",
|
|
isSelected: selection == .warranties,
|
|
action: { selection = .warranties }
|
|
)
|
|
|
|
OrganicSegmentButton(
|
|
title: L10n.Documents.documents,
|
|
icon: "doc.text",
|
|
isSelected: selection == .documents,
|
|
action: { selection = .documents }
|
|
)
|
|
}
|
|
.padding(4)
|
|
.background(Color.appBackgroundSecondary)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
.naturalShadow(.subtle)
|
|
}
|
|
}
|
|
|
|
private struct OrganicSegmentButton: View {
|
|
let title: String
|
|
let icon: String
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
Text(title)
|
|
.font(.system(size: 14, weight: .semibold))
|
|
}
|
|
.foregroundColor(isSelected ? Color.appTextOnPrimary : Color.appTextSecondary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 10)
|
|
.background(isSelected ? Color.appPrimary : Color.clear)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
|
}
|
|
.accessibilityLabel(title)
|
|
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
|
}
|
|
}
|
|
|
|
// MARK: - Organic Doc Search Bar
|
|
|
|
private struct OrganicDocSearchBar: View {
|
|
@Binding var text: String
|
|
var placeholder: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.appPrimary.opacity(0.1))
|
|
.frame(width: 32, height: 32)
|
|
Image(systemName: "magnifyingglass")
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(Color.appPrimary)
|
|
}
|
|
|
|
TextField(placeholder, text: $text)
|
|
.font(.system(size: 16, weight: .medium))
|
|
|
|
if !text.isEmpty {
|
|
Button(action: { text = "" }) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 18))
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(14)
|
|
.background(Color.appBackgroundSecondary)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.naturalShadow(.subtle)
|
|
}
|
|
}
|
|
|
|
// MARK: - Organic Doc Filter Chip
|
|
|
|
private struct OrganicDocFilterChip: View {
|
|
let title: String
|
|
var icon: String? = nil
|
|
let onRemove: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 6) {
|
|
if let icon = icon {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 12, weight: .semibold))
|
|
}
|
|
Text(title)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
|
|
Button(action: onRemove) {
|
|
Image(systemName: "xmark")
|
|
.font(.system(size: 10, weight: .bold))
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(Color.appPrimary.opacity(0.15))
|
|
.foregroundColor(Color.appPrimary)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
// MARK: - Organic Doc Toolbar Button
|
|
|
|
private struct OrganicDocToolbarButton: View {
|
|
var body: some View {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.appPrimary)
|
|
.frame(width: 32, height: 32)
|
|
|
|
Image(systemName: "plus")
|
|
.font(.system(size: 14, weight: .bold))
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting Types
|
|
|
|
extension DocumentCategory: CaseIterable {
|
|
public static var allCases: [DocumentCategory] {
|
|
return [.appliance, .hvac, .plumbing, .electrical, .roofing, .structural, .other]
|
|
}
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .appliance: return "Appliance"
|
|
case .hvac: return "HVAC"
|
|
case .plumbing: return "Plumbing"
|
|
case .electrical: return "Electrical"
|
|
case .roofing: return "Roofing"
|
|
case .structural: return "Structural"
|
|
case .other: return "Other"
|
|
default: return "Unknown"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DocumentType: CaseIterable {
|
|
public static var allCases: [DocumentType] {
|
|
return [.warranty, .manual, .receipt, .inspection, .permit, .deed, .insurance, .contract, .photo, .other]
|
|
}
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .warranty: return "Warranty"
|
|
case .manual: return "Manual"
|
|
case .receipt: return "Receipt"
|
|
case .inspection: return "Inspection"
|
|
case .permit: return "Permit"
|
|
case .deed: return "Deed"
|
|
case .insurance: return "Insurance"
|
|
case .contract: return "Contract"
|
|
case .photo: return "Photo"
|
|
case .other: return "Other"
|
|
default: return "Unknown"
|
|
}
|
|
}
|
|
}
|