"use client" import { useEffect, useState } from "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 { type DNSCheckReport, type MailDomain, type MigrationJob, type MigrationJobAuditItem, type MigrationProject, useActivateMigrationProject, useCreateMigrationInvite, useCreateMigrationProject, useCutoverMigrationProject, useImportMigrationInvites, useMicrosoftMigrationAdminConsentURL, useMigrationJobAudit, useMigrationJobAuditSummary, useMigrationProjectJobs, useMigrationProjects, 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": 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 [csvEmails, setCsvEmails] = useState("") 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 jobsQuery = useMigrationProjectJobs(activeProjectId, Boolean(activeProjectId)) const retryJob = useRetryMigrationJob(activeProjectId) const resetCursor = useResetMigrationJobCursor(activeProjectId) const retryFailed = useRetryMigrationFailedJobs(activeProjectId) const preflightDNS = usePreflightCutoverDNS(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.

)}

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" />