// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // Estimation manuelle : municipalité + type + caractéristiques facultatives, // sans adresse précise — le moteur travaille autour du centre de la municipalité. import SwiftUI struct ManualView: View { @State private var municipality = "" @State private var type: PropType = .unifamilial @State private var floorArea = "" @State private var yearBuilt = "" @State private var landArea = "" @State private var loading = false @State private var errorMessage: String? @State private var result: UnitEstimate? @State private var showResult = false var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 8) { Text("Estimation manuelle") .font(.vpDisplay(26)) .foregroundStyle(Color.vpInk) .textCase(.uppercase) Text("Pas d'adresse exacte ? Décrivez la propriété — le moteur s'appuie sur les ventes réelles de la municipalité.") .font(.vpBody(14.5)) .foregroundStyle(Color.vpInk2) } .padding(.top, 6) VStack(alignment: .leading, spacing: 16) { field(label: "Municipalité — requis") { TextField("Ex. : Québec, Lévis, Sherbrooke…", text: $municipality) .vpField() .autocorrectionDisabled() } VStack(alignment: .leading, spacing: 8) { Kicker(text: "Type de propriété — requis") typeGrid } field(label: "Aire des étages (m²) — facultatif") { TextField("Ex. : 150", text: $floorArea) .vpField() .keyboardType(.decimalPad) } field(label: "Année de construction — facultatif") { TextField("Ex. : 1985", text: $yearBuilt) .vpField() .keyboardType(.numberPad) } field(label: "Superficie du terrain (m²) — facultatif") { TextField("Ex. : 500", text: $landArea) .vpField() .keyboardType(.decimalPad) } Button { submit() } label: { HStack(spacing: 10) { if loading { ProgressView().controlSize(.small) } Text(loading ? "Calcul…" : "Estimer") } } .buttonStyle(VPPrimaryButtonStyle()) .disabled(loading || municipality.trimmingCharacters(in: .whitespaces).isEmpty) .opacity(municipality.trimmingCharacters(in: .whitespaces).isEmpty ? 0.5 : 1) if let errorMessage { Text(errorMessage) .font(.vpBody(13.5)) .foregroundStyle(Color.vpGreen) } } .padding(18) .vpCard() Text("L'estimation manuelle est moins précise qu'une estimation par adresse : elle n'utilise pas le modèle hédonique pré-calculé, seulement les comparables.") .font(.vpMono(9.5)) .foregroundStyle(Color.vpInk3) } .padding(16) } .background(Color.vpPaper) .toolbar { ToolbarItem(placement: .principal) { Wordmark(size: 20) } } .navigationBarTitleDisplayMode(.inline) .toolbarBackground(Color.vpPaper, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .navigationDestination(isPresented: $showResult) { if let result { EstimateView(data: result) } } } } private var typeGrid: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { ForEach(PropType.allCases) { t in Button { type = t } label: { Text(t.label) .font(.vpDisplayMedium(12.5)) .foregroundStyle(type == t ? Color.vpLime : Color.vpInk) .frame(maxWidth: .infinity) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 10) .fill(type == t ? Color.vpInk : Color.vpSurface2) ) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(type == t ? Color.vpInk : Color.vpInk.opacity(0.3), lineWidth: 1.5) ) } .buttonStyle(.plain) } } } private func field(label: String, @ViewBuilder content: () -> some View) -> some View { VStack(alignment: .leading, spacing: 8) { Kicker(text: label) content() } } private func submit() { guard !loading else { return } loading = true errorMessage = nil Task { defer { loading = false } do { result = try await VraiPrixAPI.shared.estimateManual( municipality: municipality.trimmingCharacters(in: .whitespaces), typeProp: type.rawValue, floorArea: Double(floorArea.replacingOccurrences(of: ",", with: ".")), yearBuilt: Int(yearBuilt), landArea: Double(landArea.replacingOccurrences(of: ",", with: ".")) ) showResult = true } catch { errorMessage = error.localizedDescription } } } } #Preview { ManualView() }