import SwiftUI struct ContactsTab: View { @State private var contacts: [Contact] = [] @State private var isLoading = false @State private var loadError: String? @State private var activeAlert: ContactsAlert? private let contactsService = ContactsService() var body: some View { List { if isLoading && contacts.isEmpty { loadingState } else if let loadError, contacts.isEmpty { errorState(loadError) } else if contacts.isEmpty { emptyState } else { Section(header: Text(NSLocalizedString("Контакты", comment: ""))) { ForEach(contacts) { contact in ContactRow(contact: contact) } } } } .listStyle(.insetGrouped) .background(Color(.systemGroupedBackground)) .task { await loadContacts() } .refreshable { await loadContacts() } .alert(item: $activeAlert) { alert in switch alert { case .error(_, let message): return Alert( title: Text(NSLocalizedString("Ошибка", comment: "Contacts load error title")), message: Text(message), dismissButton: .default(Text(NSLocalizedString("OK", comment: "Common OK"))) ) } } } private var loadingState: some View { Section { ProgressView() .frame(maxWidth: .infinity, alignment: .center) } } private func errorState(_ message: String) -> some View { Section { Text(message) .foregroundColor(.red) .frame(maxWidth: .infinity, alignment: .center) } } private var emptyState: some View { Section { VStack(spacing: 12) { Image(systemName: "person.crop.circle.badge.questionmark") .font(.system(size: 48)) .foregroundColor(.secondary) Text(NSLocalizedString("Контактов пока нет", comment: "Contacts empty state title")) .font(.headline) .multilineTextAlignment(.center) Text(NSLocalizedString("Добавьте контакты, чтобы быстрее выходить на связь.", comment: "Contacts empty state subtitle")) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 24) } } @MainActor private func loadContacts() async { if isLoading { return } isLoading = true loadError = nil do { let payloads = try await contactsService.fetchContacts() contacts = payloads.map(Contact.init) } catch { loadError = error.localizedDescription activeAlert = .error(message: error.localizedDescription) if AppConfig.DEBUG { print("[ContactsTab] load contacts failed: \(error)") } } isLoading = false } } private struct ContactRow: View { let contact: Contact var body: some View { HStack(spacing: 12) { Circle() .fill(Color.accentColor.opacity(0.15)) .frame(width: 44, height: 44) .overlay( Text(contact.initials) .font(.headline) .foregroundColor(.accentColor) ) VStack(alignment: .leading, spacing: 4) { Text(contact.displayName) .font(.body) if let handle = contact.handle { Text(handle) .font(.caption) .foregroundColor(.secondary) } HStack(spacing: 8) { if contact.friendCode { Label(NSLocalizedString("Код дружбы", comment: "Friend code badge"), systemImage: "heart.circle") .font(.caption2) .foregroundColor(.secondary) } Text(contact.formattedCreatedAt) .font(.caption2) .foregroundColor(.secondary) } } Spacer() } .padding(.vertical, 4) } } private struct Contact: Identifiable, Equatable { let id: UUID let login: String let fullName: String? let customName: String? let friendCode: Bool let createdAt: Date let displayName: String let handle: String? var initials: String { let components = displayName.split(separator: " ") let nameInitials = components.prefix(2).compactMap { $0.first } if !nameInitials.isEmpty { return nameInitials .map { String($0).uppercased() } .joined() } let filtered = login.filter { $0.isLetter }.prefix(2) if !filtered.isEmpty { return filtered.uppercased() } return "??" } var formattedCreatedAt: String { Self.relativeFormatter.localizedString(for: createdAt, relativeTo: Date()) } init(payload: ContactPayload) { self.id = payload.userId self.login = payload.login self.fullName = payload.fullName self.customName = payload.customName self.friendCode = payload.friendCode self.createdAt = payload.createdAt if let customName = payload.customName, !customName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.displayName = customName } else if let fullName = payload.fullName, !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.displayName = fullName } else { self.displayName = payload.login } if !payload.login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.handle = "@\(payload.login)" } else { self.handle = nil } } private static let relativeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .short return formatter }() } private enum ContactsAlert: Identifiable { case error(id: UUID = UUID(), message: String) var id: String { switch self { case .error(let id, _): return id.uuidString } } }