"use client" import { useEffect, useState } from "react" import { Download } from "lucide-react" import { useSearchParams } from "next/navigation" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form" import { TechBrandSelectLabel } from "@/components/admin/settings/tech-brand-select-label" import { SettingsField, SettingsGrid } from "@/components/settings/settings-kit" import { type DNSCheckReport, type MailDomain, type MigrationSharedDrive, type MigrationJob, type MigrationJobAuditItem, type MigrationProject, useActivateMigrationProject, useCreateMigrationInvite, useCreateMigrationProject, useCutoverMigrationProject, useImportMigrationInvites, useImportMigrationRoster, useMicrosoftMigrationAdminConsentURL, useMigrationJobAudit, useMigrationJobAuditSummary, useMigrationProjectJobs, useMigrationProjects, useMigrationRoster, useMigrationSharedDrives, useApproveMigrationSharedDrive, useRejectMigrationSharedDrive, useUpdateMigrationSharedDriveMode, downloadMigrationJobAudit, usePreflightCutoverDNS, useResetMigrationJobCursor, useRetryMigrationFailedJobs, useRetryMigrationJob, } from "@/lib/api/hooks/use-hosted-mail" import { ApiRequestError } from "@/lib/api/client" const SERVICE_LABELS: Record = { mail: "Mail", contacts: "Contacts", calendar: "Agenda", drive: "Drive", } const AUTH_MODE_LABELS: Record = { oauth: "OAuth utilisateur", google_dwd: "Google DWD (service account)", microsoft_app: "Microsoft app-only (client credentials)", } function statusVariant(status: string): "default" | "secondary" | "destructive" | "outline" { switch (status) { case "completed": case "active": case "cutover": case "claimed": case "invited": return "default" case "running": case "pending": return "secondary" case "failed": return "destructive" default: return "outline" } } function jobImportedCount(job: MigrationJob): string { const imported = job.stats_json?.imported if (typeof imported !== "number") return "—" const total = job.stats_json?.estimated_total if (typeof total === "number" && total > 0) { return `${Math.round(imported)} / ~${Math.round(total)}` } return String(Math.round(imported)) } export function MigrationProjectsPanel({ domains }: { domains: MailDomain[] }) { const params = useSearchParams() const projectsQuery = useMigrationProjects() const createProject = useCreateMigrationProject() const activateProject = useActivateMigrationProject() const cutoverProject = useCutoverMigrationProject() const msAdminConsent = useMicrosoftMigrationAdminConsentURL() const [projectName, setProjectName] = useState("") const [sourceProvider, setSourceProvider] = useState("google") const [authMode, setAuthMode] = useState("oauth") const [domainId, setDomainId] = useState("") const [inviteEmail, setInviteEmail] = useState("") const [alternateEmails, setAlternateEmails] = useState("") const [csvEmails, setCsvEmails] = useState("") const [rosterCsv, setRosterCsv] = useState("") const [rosterImportSummary, setRosterImportSummary] = useState(null) const [selectedProjectId, setSelectedProjectId] = useState("") const [msTenant, setMsTenant] = useState("common") const [consentBanner, setConsentBanner] = useState(null) const [cutoverReport, setCutoverReport] = useState(null) const [cutoverError, setCutoverError] = useState(null) const [dnsSource, setDnsSource] = useState<"cutover" | "persisted" | "live" | null>(null) const [auditJobId, setAuditJobId] = useState(null) const projects = projectsQuery.data?.projects ?? [] const activeProjectId = selectedProjectId || projects[0]?.id || "" const activeProject = projects.find((p) => p.id === activeProjectId) const createInvite = useCreateMigrationInvite(activeProjectId) const importInvites = useImportMigrationInvites(activeProjectId) const importRoster = useImportMigrationRoster(activeProjectId) const rosterQuery = useMigrationRoster(activeProjectId, Boolean(activeProjectId)) const jobsQuery = useMigrationProjectJobs(activeProjectId, Boolean(activeProjectId)) const retryJob = useRetryMigrationJob(activeProjectId) const resetCursor = useResetMigrationJobCursor(activeProjectId) const retryFailed = useRetryMigrationFailedJobs(activeProjectId) const preflightDNS = usePreflightCutoverDNS(activeProjectId) const updateSharedDriveMode = useUpdateMigrationSharedDriveMode(activeProjectId) const sharedDrivesQuery = useMigrationSharedDrives( activeProjectId, Boolean(activeProjectId && activeProject?.source_provider === "google") ) const approveSharedDrive = useApproveMigrationSharedDrive(activeProjectId) const rejectSharedDrive = useRejectMigrationSharedDrive(activeProjectId) useEffect(() => { const consent = params.get("microsoft_admin_consent") if (consent === "success") { const tenant = params.get("tenant") const projectId = params.get("project_id") setConsentBanner( tenant ? `Consentement admin Microsoft enregistré (tenant ${tenant}${projectId ? `, projet ${projectId.slice(0, 8)}…` : ""}).` : "Consentement admin Microsoft enregistré." ) void projectsQuery.refetch() } else if (consent === "error") { setConsentBanner("Échec du consentement admin Microsoft — vérifiez le tenant et réessayez.") void projectsQuery.refetch() } }, [params, projectsQuery]) useEffect(() => { setCutoverReport(null) setCutoverError(null) setDnsSource(null) setAuditJobId(null) }, [activeProjectId]) useEffect(() => { if (activeProject?.cutover_dns) { setDnsSource((prev) => (prev === "cutover" || prev === "live" ? prev : "persisted")) } }, [activeProject?.cutover_dns, activeProjectId]) const dnsReport = cutoverReport ?? activeProject?.cutover_dns ?? preflightDNS.data?.dns ?? null const failedCount = (jobsQuery.data?.jobs ?? []).filter((j) => j.status === "failed").length return ( {consentBanner && (

{consentBanner}

)} setProjectName(e.target.value)} placeholder="Migration ACME 2026" />
    {projects.map((project) => ( setSelectedProjectId(project.id)} onActivate={() => { setSelectedProjectId(project.id) void activateProject.mutateAsync(project.id) }} onCutover={() => { setCutoverError(null) void cutoverProject.mutateAsync(project.id).then( (res) => { setCutoverReport(res.dns) setDnsSource("cutover") }, (err) => { setCutoverReport(null) if (err instanceof ApiRequestError && err.code === "migration_cutover_mx_not_ready") { const details = err.details as { dns?: DNSCheckReport } | undefined if (details?.dns) { setCutoverReport(details.dns) setDnsSource("cutover") } setCutoverError(err.message) } else { setCutoverError(err instanceof Error ? err.message : "Cutover failed") } } ) }} /> ))}
{activeProject?.source_provider === "microsoft" && (

Consentement admin Microsoft

Requis pour les migrations Microsoft 365 {activeProject.auth_mode === "microsoft_app" ? " en mode app-only (permissions application : Mail.Read, Calendars.Read, Contacts.Read, Files.Read.All)." : " en mode organisation (permissions applicatives)."}

{activeProject.microsoft_admin_consent_at ? (

Consentement accordé {activeProject.microsoft_tenant_id ? ` — tenant ${activeProject.microsoft_tenant_id}` : ""} {activeProject.microsoft_admin_consent_at ? ` (${new Date(activeProject.microsoft_admin_consent_at).toLocaleString()})` : ""}

) : activeProject.microsoft_admin_consent_error ? (

{activeProject.microsoft_admin_consent_error}

) : (

Consentement non enregistré pour ce projet.

)}
setMsTenant(e.target.value)} placeholder="common ou tenant ID" />
)} {activeProjectId && ( <>

Pré-vérification DNS (cutover)

{cutoverError && (

{cutoverError}

)} {dnsReport && ( <> {dnsSource === "persisted" && activeProject?.cutover_at && (

Rapport enregistré au cutover ( {new Date(activeProject.cutover_at).toLocaleString()}).

)} {dnsSource === "live" && (

Vérification DNS live (non enregistrée — lancez la bascule MX pour persister).

)} )} {!dnsReport && activeProject?.domain_id && (

Aucun rapport DNS enregistré — vérifiez live ou lancez la bascule MX.

)}
{activeProject?.source_provider === "google" && ( void updateSharedDriveMode.mutateAsync(mode)} modePending={updateSharedDriveMode.isPending} onApprove={(driveId) => void approveSharedDrive.mutateAsync(driveId)} onReject={(driveId) => void rejectSharedDrive.mutateAsync(driveId)} actionPending={approveSharedDrive.isPending || rejectSharedDrive.isPending} /> )}

Jobs de migration

setAuditJobId((prev) => (prev === jobId ? null : jobId))} onRetry={(jobId) => void retryJob.mutateAsync(jobId)} onResetCursor={(jobId) => void resetCursor.mutateAsync(jobId)} retryPending={retryJob.isPending} resetPending={resetCursor.isPending} /> {auditJobId && ( )}

Invitations utilisateurs

setInviteEmail(e.target.value)} placeholder="alice@entreprise.com" />