// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // SourcesView.swift : registre des gestionnaires (statut, annonces, dernière sync) // ----------------------------------------------------------------------------- import SwiftUI struct SourcesView: View { @Environment(AppModel.self) private var model @State private var query = "" private var actives: [SourceInfo] { model.sources.filter { $0.activeListings > 0 } } private var filtered: [SourceInfo] { let base = model.sources.sorted { ($0.activeListings, $1.name) > ($1.activeListings, $0.name) } guard !query.isEmpty else { return base } return base.filter { $0.name.localizedCaseInsensitiveContains(query) || $0.id.localizedCaseInsensitiveContains(query) } } var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 14) { Kicker(text: "Registre des gestionnaires") HStack(spacing: 10) { counter(value: model.sources.count, label: "sources recensées") counter(value: actives.count, label: "avec annonces") } ForEach(filtered) { source in row(source) } } .padding(16) .padding(.bottom, 24) } .background(LK.paper) .navigationTitle("Sources") .navigationBarTitleDisplayMode(.inline) .searchable(text: $query, prompt: "Chercher un gestionnaire") .refreshable { await model.loadGlobals(force: true) } .overlay { if model.sources.isEmpty { ProgressView("Chargement…") } } } } private func counter(value: Int, label: String) -> some View { VStack(alignment: .leading, spacing: 2) { Text(Fmt.int(value)) .font(LKFont.display(24, .bold)) Text(label.uppercased()) .font(LKFont.mono(9, .medium)) .kerning(0.5) .foregroundStyle(LK.ink3) } .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .background(LK.surface) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(LK.line, lineWidth: 1)) } private func row(_ source: SourceInfo) -> some View { let connected = source.activeListings > 0 return HStack(spacing: 12) { Circle() .fill(connected ? LK.green : LK.amber) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: 2) { Text(source.name) .font(LKFont.display(15, .medium)) .lineLimit(1) HStack(spacing: 6) { Text(source.id) .font(LKFont.mono(10)) .foregroundStyle(LK.ink3) if let sync = Fmt.relative(source.lastSync) { Text("· sync \(sync)") .font(.system(size: 10.5)) .foregroundStyle(LK.ink3) } } } Spacer() if connected { Text("\(source.activeListings)") .font(LKFont.mono(12, .bold)) .padding(.horizontal, 8) .padding(.vertical, 4) .background(LK.limeSoft) .foregroundStyle(LK.greenDeep) .clipShape(Capsule()) } else { Text(source.status) .font(LKFont.mono(9.5, .medium)) .foregroundStyle(LK.amber) .lineLimit(1) .frame(maxWidth: 90, alignment: .trailing) } } .padding(.horizontal, 13) .padding(.vertical, 10) .background(LK.surface) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(LK.line, lineWidth: 1)) } }