Support.swift 184 lignes · 6348 octets
import SwiftUI
import UIKit

// Shared UI helpers: display names, top bars, path-based images, external
// links and draft image persistence.

// Kotlin `contactDisplayName` (CarnetScreen/ContactPagerScreen).
func contactDisplayName(_ c: CrmContactEntity) -> String {
    if let full = c.fullName?.nilIfBlank { return full }
    let composed = [c.firstName, c.lastName].compactMap { $0 }
        .joined(separator: " ")
        .trimmingCharacters(in: .whitespaces)
    if !composed.isEmpty { return composed }
    if let company = c.company?.nilIfBlank { return company }
    return "—"
}

// Kotlin `contactSubtitle` (CarnetScreen).
func contactSubtitle(_ c: CrmContactEntity) -> String? {
    let name: String?
    if let full = c.fullName?.nilIfBlank {
        name = full
    } else {
        let composed = [c.firstName, c.lastName].compactMap { $0 }
            .joined(separator: " ")
            .trimmingCharacters(in: .whitespaces)
        name = composed.isEmpty ? nil : composed
    }
    if name != nil {
        return c.company?.nilIfBlank
    }
    return c.jobTitle?.nilIfBlank
}

/// Custom top bar mirroring the Android screens' own header rows
/// (system navigation bars are hidden on ported screens).
struct ScreenTopBar<Trailing: View>: View {
    let title: String
    var centered: Bool = true
    let onBack: () -> Void
    @ViewBuilder var trailing: () -> Trailing

    init(
        title: String,
        centered: Bool = true,
        onBack: @escaping () -> Void,
        @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() }
    ) {
        self.title = title
        self.centered = centered
        self.onBack = onBack
        self.trailing = trailing
    }

    var body: some View {
        ZStack {
            HStack(spacing: 4) {
                Button(action: onBack) {
                    Image(systemName: "arrow.left")
                        .foregroundColor(C2VColor.ink)
                        .frame(width: 44, height: 44)
                }
                .accessibilityLabel("Retour")
                if !centered {
                    Text(title)
                        .font(C2VFont.titleMedium)
                        .foregroundColor(C2VColor.encre)
                }
                Spacer()
                trailing()
            }
            if centered {
                Text(title)
                    .font(C2VFont.titleMedium)
                    .foregroundColor(C2VColor.encre)
            }
        }
        .padding(.horizontal, 4)
        .padding(.vertical, 4)
        .background(C2VColor.surface)
    }
}

/// Kotlin `PathImage` — renders nothing when the file is missing.
struct PathImage: View {
    let path: String?
    var contentMode: ContentMode = .fill
    /// Bumps identity when the underlying file changed (Kotlin `revision`).
    var revision: Int64 = 0

    var body: some View {
        if let path = path?.nilIfBlank, let ui = UIImage(contentsOfFile: path) {
            Image(uiImage: ui)
                .resizable()
                .aspectRatio(contentMode: contentMode)
                .id("\(path)#\(revision)")
        }
    }
}

/// Android `ACTION_DIAL` / `ACTION_SENDTO` equivalents.
enum ExternalLinks {
    static func dial(_ phone: String) {
        let tel = phone.filter { $0.isNumber || $0 == "+" }
        guard !tel.isEmpty, let url = URL(string: "tel:\(tel)") else { return }
        UIApplication.shared.open(url)
    }

    static func email(_ address: String) {
        let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? address
        guard let url = URL(string: "mailto:\(encoded)") else { return }
        UIApplication.shared.open(url)
    }

    static func web(_ site: String) {
        var raw = site.trimmingCharacters(in: .whitespaces)
        if !raw.lowercased().hasPrefix("http://") && !raw.lowercased().hasPrefix("https://") {
            raw = "https://\(raw)"
        }
        guard let url = URL(string: raw) else { return }
        UIApplication.shared.open(url)
    }
}

/// Persists UI-captured images as JPEG files; the repository stores paths only
/// (Android saved Bitmaps through `ContactImageStore`).
enum DraftImageWriter {
    static func writeJpeg(_ image: UIImage, prefix: String) -> String? {
        guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
        let fm = FileManager.default
        guard
            let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
        else { return nil }
        let dir = base.appendingPathComponent("images", isDirectory: true)
        do {
            try fm.createDirectory(at: dir, withIntermediateDirectories: true)
            let url = dir.appendingPathComponent("\(prefix)-\(UUID().uuidString).jpg")
            try data.write(to: url, options: .atomic)
            return url.path
        } catch {
            return nil
        }
    }
}

/// `UIImagePickerController` camera wrapper for the profile-photo capture
/// (Android `ActivityResultContracts.TakePicture`).
struct CameraPickerView: UIViewControllerRepresentable {
    let completion: (UIImage?) -> Void

    static var isAvailable: Bool {
        UIImagePickerController.isSourceTypeAvailable(.camera)
    }

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let controller = UIImagePickerController()
        controller.sourceType = .camera
        controller.delegate = context.coordinator
        return controller
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(completion: completion) }

    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        private let completion: (UIImage?) -> Void

        init(completion: @escaping (UIImage?) -> Void) {
            self.completion = completion
        }

        func imagePickerController(
            _ picker: UIImagePickerController,
            didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
        ) {
            completion(info[.originalImage] as? UIImage)
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            completion(nil)
        }
    }
}