// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // ProfileView.swift — profil : KA ID (compte unique de l'écosystème), univers // favoris, apparence, contacts complets du Groupe KA, pages légales, à propos. import SwiftUI struct ProfileView: View { @AppStorage("ka.favUniverses") private var favUniversesRaw = "" @AppStorage("ka.appearance") private var appearance = "clair" @Environment(\.colorScheme) private var scheme @StateObject private var kaid = KAIDManager.shared @EnvironmentObject private var favorites: FavoritesStore @EnvironmentObject private var recents: RecentsStore @StateObject private var perso = PersonalizationStore.shared @AppStorage(PersonalizationStore.enabledKey) private var persoEnabled = true @State private var showHubSite = false @State private var confirmReset = false private var favIDs: Set { Set(favUniversesRaw.split(separator: ",").map(String.init)) } var body: some View { NavigationStack { List { Section { if let p = kaid.profile { KAIDCard(profile: p) .listRowInsets(EdgeInsets()) .listRowBackground(Color.clear) Link(destination: Ecosystem.hubURL.appendingPathComponent("/compte")) { Label("Gérer mon profil sur groupe-ka.com", systemImage: "person.text.rectangle") } Button(role: .destructive) { kaid.logout() } label: { Label("Se déconnecter", systemImage: "rectangle.portrait.and.arrow.right") } } else { HStack(spacing: 14) { ZStack { RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(KATheme.inkLight) .frame(width: 56, height: 56) Text("KA").font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundStyle(KATheme.lime) .rotationEffect(.degrees(-4)) } VStack(alignment: .leading, spacing: 3) { Text("Votre KA ID").font(.headline) Text("Un seul compte pour les 13 plateformes de l'écosystème.") .font(.caption).foregroundStyle(.secondary) } } Button { kaid.login() } label: { HStack { Label("Se connecter avec KA ID", systemImage: "person.crop.circle.badge.checkmark") Spacer() if kaid.busy { ProgressView() } } } .disabled(kaid.busy) Link(destination: Ecosystem.signupURL) { Label("Créer un compte sur groupe-ka.com", systemImage: "arrow.up.right.square") } if let e = kaid.lastError { Text(e).font(.caption).foregroundStyle(.red) } } } Section("Mes contenus") { NavigationLink { FavoritesView() } label: { Label("Favoris & collections", systemImage: "heart.fill") .badge(favorites.allItems.count) } NavigationLink { HistoryView() } label: { Label("Historique consulté", systemImage: "clock.arrow.circlepath") .badge(recents.viewed.count) } NavigationLink { SavedSearchesView() } label: { Label("Recherches sauvegardées & alertes", systemImage: "bell.badge") .badge(recents.alertCount > 0 ? "\(recents.alertCount) nouv." : "") } } Section { Toggle(isOn: $persoEnabled) { Label("Recommandations personnalisées", systemImage: "sparkles") } if persoEnabled, perso.profile.hasSignals { let p = perso.profile if !p.affinity.isEmpty { LabeledContent("Univers suivis", value: p.topUniverses(3) .compactMap { Ecosystem.universe($0)?.name } .joined(separator: ", ")) } if !p.topCities.isEmpty { LabeledContent("Villes", value: p.topCities.prefix(3).joined(separator: ", ")) } LabeledContent("Signaux enregistrés", value: "\(p.eventCount)") } Button(role: .destructive) { confirmReset = true } label: { Label("Effacer ce que KA a appris", systemImage: "trash") } .disabled(perso.events.isEmpty) } header: { Text("Personnalisation") } footer: { Text("Vos consultations, favoris et recherches personnalisent l'accueil et le feed « Pour vous ». Tout reste sur cet appareil — rien n'est envoyé aux serveurs.") } Section { ForEach(Ecosystem.all) { u in Button { var ids = favIDs if ids.contains(u.id) { ids.remove(u.id) } else { ids.insert(u.id) } favUniversesRaw = ids.sorted().joined(separator: ",") Haptics.tap() } label: { HStack { KALogo(universe: u, size: 26) Text(u.name).foregroundStyle(.primary) Spacer() if favIDs.contains(u.id) { Image(systemName: "star.fill").foregroundStyle(.yellow) } } } } } header: { Text("Mes univers favoris") } footer: { Text("Vos univers favoris remontent en tête de l'accueil.") } Section("Apparence") { Picker("Thème", selection: $appearance) { Text("Automatique").tag("auto") Text("Clair").tag("clair") Text("Sombre").tag("sombre") } } Section("Joindre le Groupe KA") { ForEach(Ecosystem.contacts, id: \.email) { c in Link(destination: URL(string: "mailto:\(c.email)")!) { VStack(alignment: .leading, spacing: 2) { Text(c.email).font(.subheadline.weight(.semibold)) Text(c.role).font(.caption).foregroundStyle(.secondary) } } } Button { showHubSite = true } label: { HStack(spacing: 10) { KALogo(assetID: "groupe-ka", size: 22) Text("groupe-ka.com — le portail") } } Link(destination: Ecosystem.statusURL) { Label("État des services en direct", systemImage: "waveform.path.ecg") } } Section("Légal") { ForEach(Ecosystem.legal, id: \.path) { l in Link(destination: Ecosystem.hubURL.appendingPathComponent(l.path)) { Text(l.label) } } Text(Ecosystem.disclaimer) .font(.caption2).foregroundStyle(.secondary) } Section("À propos") { LabeledContent("Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0") LabeledContent("Écosystème", value: "\(Ecosystem.all.count) univers") Text("KA — tout l'écosystème Groupe-KA dans votre poche. Fait au Québec. 💚") .font(.caption).foregroundStyle(.secondary) } } .navigationTitle("Profil") .scrollContentBackground(.hidden) .background(KATheme.paper(scheme)) .fullScreenCover(isPresented: $showHubSite) { SiteBrowserCover(title: "Groupe KA", accent: KATheme.green, logoID: "groupe-ka", url: Ecosystem.hubURL) } .confirmationDialog("Effacer les données d'apprentissage ?", isPresented: $confirmReset, titleVisibility: .visible) { Button("Tout effacer", role: .destructive) { perso.reset() Haptics.success() } } message: { Text("Le feed « Pour vous » repartira de zéro. Vos favoris et votre historique ne sont pas touchés.") } } } } // MARK: - Historique consulté struct HistoryView: View { @EnvironmentObject private var recents: RecentsStore @Environment(\.colorScheme) private var scheme var body: some View { ScrollView { LazyVStack(spacing: 10) { if recents.viewed.isEmpty { KAEmptyState(symbol: "clock", title: "Rien de consulté encore", message: "Les fiches que vous ouvrez apparaîtront ici.") } ForEach(recents.viewed) { item in NavigationLink(value: item) { KAItemRow(item: item) } .buttonStyle(KAPressStyle()) } } .padding(14) } .background(KATheme.paper(scheme)) .navigationTitle("Historique") .navigationDestination(for: KAItem.self) { ItemDetailView(item: $0) } .toolbar { if !recents.viewed.isEmpty { ToolbarItem(placement: .topBarTrailing) { Button("Effacer") { recents.clearHistory() } } } } } } // MARK: - Recherches sauvegardées & alertes struct SavedSearchesView: View { @EnvironmentObject private var recents: RecentsStore @Environment(\.colorScheme) private var scheme var body: some View { List { if recents.savedSearches.isEmpty { KAEmptyState(symbol: "bell.slash", title: "Aucune recherche sauvegardée", message: "Dans Recherche, touchez 🔔 pour suivre une recherche : KA recomptera les résultats et affichera les nouveautés.") .listRowBackground(Color.clear) } ForEach($recents.savedSearches) { $s in VStack(alignment: .leading, spacing: 6) { HStack { Text(s.name).font(.headline) if s.newCount > 0 { Text("+\(s.newCount) nouveautés") .font(.system(size: 10, design: .monospaced).weight(.bold)) .padding(.horizontal, 7).padding(.vertical, 3) .background(.red, in: Capsule()).foregroundStyle(.white) } Spacer() Toggle("", isOn: $s.alertsOn).labelsHidden() .accessibilityLabel("Alerte pour \(s.name)") } HStack(spacing: 8) { if let u = Ecosystem.universe(s.universeID) { KAChip(text: u.wordmark, accent: u.accent) } Text("« \(s.query) »").font(.caption).foregroundStyle(.secondary) Spacer() if let total = s.lastTotal { Text("\(total + s.newCount) résultats") .font(.system(.caption2, design: .monospaced)) .foregroundStyle(.tertiary) } } if s.newCount > 0 { Button("Marquer comme vues") { recents.markSeen(s.id) } .font(.caption.weight(.semibold)) } } .swipeActions { Button(role: .destructive) { recents.remove(s.id) } label: { Label("Supprimer", systemImage: "trash") } } } } .navigationTitle("Recherches & alertes") .refreshable { await recents.refreshAlerts() } .scrollContentBackground(.hidden) .background(KATheme.paper(scheme)) } }