db65db6232
Android UI Tests / ui-tests (push) Has been cancelled
Localize all user-facing strings across iOS (SwiftUI), shared Kotlin, and Android Compose into en/es/fr/de/pt/it/ja/ko/nl/zh: - iOS String Catalogs: main + widget Localizable.xcstrings, InfoPlist.xcstrings (permissions), plural variations, ~200 new keys translated - Shared Kotlin ClientStrings table + Android composeResources/values-* (884 keys ×10), routed Api/ViewModel/util error & UI strings through localization - Backend-localized lookups/suggestions consumed via display names - Widget extension catalog; theme names, home-profile fallbacks, validation, network errors, accessibility labels all localized Add re-runnable verification gates: - scripts/i18n_audit.py — enumerate every literal, partition to GAP=0 - scripts/i18n_coverage.py — all 10 locales translated, format-specifier parity Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
399 lines
15 KiB
Swift
399 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 ? String(localized: "Show all warranties") : String(localized: "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 { // i18n-ignore: NotificationCenter userInfo key (non-UI)
|
|
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 {
|
|
DocumentCategoryHelper.displayName(for: self.value)
|
|
}
|
|
}
|
|
|
|
extension DocumentType: CaseIterable {
|
|
public static var allCases: [DocumentType] {
|
|
return [.warranty, .manual, .receipt, .inspection, .permit, .deed, .insurance, .contract, .photo, .other]
|
|
}
|
|
|
|
var displayName: String {
|
|
DocumentTypeHelper.displayName(for: self.value)
|
|
}
|
|
}
|