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) } }
GitRust