ProjetsListScreen.swift 199 lignes · 7592 octets
import SwiftUI

// Port of ui/projets/ProjetsListScreen.kt — header, Carnet | Projets tabs,
// hairline-divided project rows.

struct ProjetsListScreen: View {
    @ObservedObject var viewModel: ProjetsViewModel
    let onOpenCarnet: () -> Void
    let onOpenProjet: (String) -> Void
    let onSettings: () -> Void
    @ObservedObject var syncViewModel: SyncChromeViewModel

    @State private var showCreateDialog = false

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                Text("Projets".uppercased())
                    .font(C2VFont.titleLarge)
                    .foregroundColor(C2VColor.ink)
                Spacer()
                if syncViewModel.configured {
                    Button(action: { syncViewModel.syncNow() }) {
                        Image(systemName: "arrow.triangle.2.circlepath")
                            .foregroundColor(C2VColor.ink)
                            .frame(width: 44, height: 44)
                    }
                    .accessibilityLabel("Synchroniser")
                }
                Button(action: onSettings) {
                    Image(systemName: "gearshape")
                        .foregroundColor(C2VColor.ink)
                        .frame(width: 44, height: 44)
                }
                .accessibilityLabel("Paramètres")
            }
            .padding(.top, 12)
            .padding(.bottom, 8)

            MainTabRow(current: .projets) { tab in
                if tab == .carnet { onOpenCarnet() }
            }

            SyncBanner(
                pendingChanges: syncViewModel.pendingRemoteChanges,
                localAhead: syncViewModel.localCalendarAhead,
                syncing: syncViewModel.syncing,
                error: syncViewModel.error,
                onSyncClick: { syncViewModel.syncNow() },
                summary: syncViewModel.syncSummary
            )

            Spacer().frame(height: 8)

            if viewModel.projets.isEmpty {
                Spacer()
                Text("Aucun projet — synchronisez pour les récupérer")
                    .font(C2VFont.bodyLarge)
                    .foregroundColor(C2VColor.texteFaible)
                    .multilineTextAlignment(.center)
                Spacer()
            } else {
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        ForEach(viewModel.projets, id: \.localId) { projet in
                            ProjetRow(projet: projet) {
                                if let serverId = projet.serverId {
                                    onOpenProjet(serverId)
                                }
                            }
                        }
                    }
                }
            }
        }
        .padding(.horizontal, 18)
        .background(C2VColor.fond)
        .overlay(alignment: .bottomTrailing) {
            Button(action: { showCreateDialog = true }) {
                Image(systemName: "plus")
                    .foregroundColor(C2VColor.onPrimary)
                    .frame(width: 56, height: 56)
                    .background(C2VColor.ink)
            }
            .accessibilityLabel("Créer un projet")
            .padding(.trailing, 18)
            .padding(.bottom, 24)
        }
        .overlay {
            if showCreateDialog {
                CreateProjetDialog(
                    workflows: viewModel.workflows,
                    onCreate: { nom, description, workflowServerId in
                        viewModel.createProjet(
                            nom: nom,
                            description: description,
                            workflowServerId: workflowServerId
                        )
                        showCreateDialog = false
                    },
                    onDismiss: { showCreateDialog = false }
                )
            }
        }
        .syncConflictDialog(
            conflict: syncViewModel.conflictPending,
            onConfirm: { syncViewModel.confirmConflictCancellation() },
            onDismiss: { syncViewModel.dismissConflict() }
        )
        .toolbar(.hidden, for: .navigationBar)
        .task { await viewModel.refresh() }
        .onAppear { syncViewModel.refreshStatus() }
    }
}

private struct CreateProjetDialog: View {
    let workflows: [WorkflowEntity]
    let onCreate: (String, String, String) -> Void
    let onDismiss: () -> Void

    @State private var nom = ""
    @State private var description = ""
    @State private var workflowIndex = 0

    private var selected: WorkflowEntity? {
        workflows.indices.contains(workflowIndex) ? workflows[workflowIndex] : workflows.first
    }

    var body: some View {
        C2VDialog(
            title: "Créer un projet",
            confirmLabel: "Créer",
            dismissLabel: "Annuler",
            onConfirm: {
                guard nom.isNotBlank, let selected else { return }
                onCreate(nom, description, selected.serverId)
            },
            onDismiss: onDismiss
        ) {
            VStack(alignment: .leading, spacing: 8) {
                C2VTextField(label: "Nom du projet", text: $nom)
                C2VTextField(label: "Description", text: $description)
                if workflows.isEmpty {
                    Text("Synchronisez d'abord pour récupérer les workflows")
                        .font(C2VFont.bodyMedium)
                        .foregroundColor(C2VColor.texteFaible)
                } else {
                    Menu {
                        ForEach(Array(workflows.enumerated()), id: \.offset) { index, workflow in
                            Button(workflow.nom) { workflowIndex = index }
                        }
                    } label: {
                        HStack(spacing: 4) {
                            Text("Workflow : \(selected?.nom ?? "")")
                                .font(C2VFont.labelMedium)
                                .foregroundColor(C2VColor.ink)
                            Image(systemName: "chevron.down")
                                .foregroundColor(C2VColor.texteFaible)
                        }
                        .padding(.vertical, 8)
                        .padding(.horizontal, 12)
                        .overlay(Rectangle().stroke(C2VColor.hairline, lineWidth: 1))
                    }
                }
            }
        }
    }
}

private struct ProjetRow: View {
    let projet: ProjetEntity
    let onTap: () -> Void

    var body: some View {
        Button(action: onTap) {
            VStack(alignment: .leading, spacing: 0) {
                VStack(alignment: .leading, spacing: 2) {
                    Text(projet.nom.nilIfBlank ?? "—")
                        .font(C2VFont.bodyLarge)
                        .foregroundColor(C2VColor.ink)
                    if projet.description.isNotBlank {
                        Text(projet.description)
                            .font(C2VFont.bodyMedium)
                            .foregroundColor(C2VColor.texteFaible)
                    }
                    if projet.serverId == nil {
                        Text("En attente de synchro")
                            .font(C2VFont.bodyMedium)
                            .foregroundColor(C2VColor.texteFaible)
                    }
                }
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity, alignment: .leading)
                C2VDivider()
            }
        }
        .buttonStyle(.plain)
    }
}