"use client" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { apiClient } from "@/lib/api/client" export type MailDomain = { id: string name: string status: string verification_token?: string dkim_selector?: string dkim_public_key?: string is_platform_domain: boolean mx_verified_at?: string | null txt_verified_at?: string | null created_at: string } export type MigrationProject = { id: string domain_id?: string name: string source_provider: string auth_mode: string status: string cutover_at?: string | null delta_mode: boolean shared_drive_mode?: "auto" | "manual" created_at: string microsoft_tenant_id?: string microsoft_admin_consent_at?: string | null microsoft_admin_consent_error?: string cutover_dns?: DNSCheckReport } export type MigrationSharedDrive = { id: string project_id: string drive_id: string name: string status: "pending" | "approved" | "rejected" discovered_by_user_id?: string | null created_at: string updated_at: string } export type MicrosoftAdminConsent = { tenant_id: string client_id: string project_id?: string granted: boolean error_code?: string error_description?: string consented_at: string updated_at: string } export type MigrationJob = { id: string project_id: string user_id: string service: string status: string stats_json?: Record error?: string user_email?: string started_at?: string | null completed_at?: string | null } export type MigrationJobAuditItem = { source_id: string rel_path?: string status: "imported" | "failed" | "skipped" reason?: string imported_at: string } export type MigrationJobAuditSummary = { service: string imported: number failed: number skipped: number total: number by_status?: Record } export type MigrationOnboardingHints = { needs_user_oauth: boolean oauth_provider?: string waiting_for_admin: boolean waiting_reason?: string has_migration_credentials?: boolean needs_microsoft_admin_consent?: boolean } export type MigrationUserStatus = { project?: MigrationProject invite?: MigrationInvite jobs?: MigrationJob[] onboarding?: MigrationOnboardingHints } export type MigrationInviteResponse = { invite: MigrationInvite project: MigrationProject onboarding?: MigrationOnboardingHints } export type MigrationInvite = { id: string project_id: string email: string alternate_emails?: string[] token?: string status: string } export type MigrationRosterEntry = { id: string project_id: string email: string display_name?: string alternate_emails?: string[] status: "pending" | "invited" | "claimed" invite_id?: string created_at: string updated_at: string } export type MigrationRosterImportResult = { created: number skipped_duplicates: number errors?: Array<{ row: number; email?: string; message: string }> } export type DNSCheckReport = { domain?: string txt_verified: boolean txt_records?: string[] txt_expected?: string mx_verified: boolean mx_records?: string[] expected_mx?: string[] warnings?: string[] errors?: string[] } export type MigrationCutoverResult = { project: MigrationProject dns: DNSCheckReport } export function useMailDomains() { return useQuery({ queryKey: ["admin", "mail", "domains"], queryFn: () => apiClient.get<{ domains: MailDomain[] }>("/admin/mail/domains"), }) } export function useCreateMailDomain() { const queryClient = useQueryClient() return useMutation({ mutationFn: (body: { name: string }) => apiClient.post("/admin/mail/domains", body), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] }) }, }) } export function useVerifyMailDomainTXT(domainId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: () => apiClient.post(`/admin/mail/domains/${domainId}/verify-txt`), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] }) }, }) } export function useVerifyMailDomainMX(domainId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: () => apiClient.post(`/admin/mail/domains/${domainId}/verify-mx`), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] }) }, }) } export function useMigrationProjects() { return useQuery({ queryKey: ["admin", "migration", "projects"], queryFn: () => apiClient.get<{ projects: MigrationProject[] }>("/admin/migration/projects"), }) } export function useCreateMigrationProject() { const queryClient = useQueryClient() return useMutation({ mutationFn: (body: { name: string source_provider: string domain_id?: string auth_mode?: string }) => apiClient.post("/admin/migration/projects", body), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] }) }, }) } export function useActivateMigrationProject() { const queryClient = useQueryClient() return useMutation({ mutationFn: (projectId: string) => apiClient.post(`/admin/migration/projects/${projectId}/activate`), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] }) }, }) } export function useCutoverMigrationProject() { const queryClient = useQueryClient() return useMutation({ mutationFn: (projectId: string) => apiClient.post(`/admin/migration/projects/${projectId}/cutover`), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] }) }, }) } export function usePreflightCutoverDNS(projectId: string) { return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "cutover-dns"], enabled: Boolean(projectId), queryFn: () => apiClient.get<{ dns: DNSCheckReport }>( `/admin/migration/projects/${projectId}/cutover-dns` ), }) } export function useCreateMigrationInvite(projectId: string) { return useMutation({ mutationFn: (body: { email: string; alternate_emails?: string[] }) => apiClient.post(`/admin/migration/projects/${projectId}/invites`, body), }) } export function useImportMigrationInvites(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (emails: string[]) => apiClient.post<{ imported: number }>( `/admin/migration/projects/${projectId}/invites/import`, { emails } ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] }) }, meta: { requiresProjectId: projectId }, }) } export function useMigrationRoster(projectId: string, enabled = true) { return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "roster"], enabled: Boolean(projectId) && enabled, queryFn: () => apiClient.get<{ roster: MigrationRosterEntry[] }>( `/admin/migration/projects/${projectId}/roster` ), }) } export function useImportMigrationRoster(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (csv: string) => apiClient.post( `/admin/migration/projects/${projectId}/roster`, { csv } ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "roster"], }) }, meta: { requiresProjectId: projectId }, }) } export function useMigrationProjectJobs(projectId: string, enabled = true) { return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "jobs"], enabled: Boolean(projectId) && enabled, queryFn: () => apiClient.get<{ jobs: MigrationJob[] }>( `/admin/migration/projects/${projectId}/jobs` ), refetchInterval: 10_000, }) } export function useMigrationJobAuditSummary(projectId: string, jobId: string | null) { return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "jobs", jobId, "audit", "summary"], enabled: Boolean(projectId && jobId), queryFn: () => apiClient.get( `/admin/migration/projects/${projectId}/jobs/${jobId}/audit/summary` ), refetchInterval: 10_000, }) } export function useMigrationJobAudit( projectId: string, jobId: string | null, status: "failed" | "skipped" | "imported" | "all" = "failed" ) { const statusParam = status === "all" ? "" : status return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "jobs", jobId, "audit", status], enabled: Boolean(projectId && jobId), queryFn: () => { const params = new URLSearchParams({ page_size: "50" }) if (statusParam) params.set("status", statusParam) return apiClient.get<{ items: MigrationJobAuditItem[]; pagination: { total?: number } }>( `/admin/migration/projects/${projectId}/jobs/${jobId}/audit?${params.toString()}` ) }, refetchInterval: 10_000, }) } export async function downloadMigrationJobAudit( projectId: string, jobId: string, format: "csv" | "ndjson" ) { const blob = await apiClient.getBlob( `/admin/migration/projects/${projectId}/jobs/${jobId}/audit/export?format=${format}` ) const url = URL.createObjectURL(blob) const anchor = document.createElement("a") anchor.href = url anchor.download = `migration-job-audit-${jobId.slice(0, 8)}.${format === "csv" ? "csv" : "ndjson"}` anchor.click() URL.revokeObjectURL(url) } export async function downloadMigrationProjectAudit( projectId: string, format: "csv" | "ndjson" ) { const blob = await apiClient.getBlob( `/admin/migration/projects/${projectId}/audit/export?format=${format}` ) const url = URL.createObjectURL(blob) const anchor = document.createElement("a") anchor.href = url anchor.download = `migration-project-audit-${projectId.slice(0, 8)}.${format === "csv" ? "csv" : "ndjson"}` anchor.click() URL.revokeObjectURL(url) } export function useUpdateMigrationSharedDriveMode(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (shared_drive_mode: "auto" | "manual") => apiClient.patch( `/admin/migration/projects/${projectId}/shared-drive-mode`, { shared_drive_mode } ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] }) void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "shared-drives"], }) }, }) } export function useMigrationSharedDrives(projectId: string, enabled = true) { return useQuery({ queryKey: ["admin", "migration", "projects", projectId, "shared-drives"], enabled: Boolean(projectId) && enabled, queryFn: () => apiClient.get<{ shared_drives: MigrationSharedDrive[] }>( `/admin/migration/projects/${projectId}/shared-drives` ), refetchInterval: 15_000, }) } export function useApproveMigrationSharedDrive(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (driveId: string) => apiClient.post( `/admin/migration/projects/${projectId}/shared-drives/${encodeURIComponent(driveId)}/approve` ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "shared-drives"], }) }, }) } export function useRejectMigrationSharedDrive(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (driveId: string) => apiClient.post( `/admin/migration/projects/${projectId}/shared-drives/${encodeURIComponent(driveId)}/reject` ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "shared-drives"], }) }, }) } export function useRetryMigrationJob(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (jobId: string) => apiClient.post( `/admin/migration/projects/${projectId}/jobs/${jobId}/retry` ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "jobs"], }) }, }) } export function useResetMigrationJobCursor(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: (jobId: string) => apiClient.post( `/admin/migration/projects/${projectId}/jobs/${jobId}/reset-cursor` ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "jobs"], }) }, }) } export function useRetryMigrationFailedJobs(projectId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: () => apiClient.post<{ retried: number }>( `/admin/migration/projects/${projectId}/jobs/retry-failed` ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects", projectId, "jobs"], }) }, }) } export function useMicrosoftMigrationAdminConsentURL() { return useMutation({ mutationFn: (body: { tenant?: string; projectId?: string }) => { const params = new URLSearchParams() if (body.tenant?.trim()) params.set("tenant", body.tenant.trim()) if (body.projectId) params.set("project_id", body.projectId) const query = params.toString() return apiClient.get<{ url: string }>( `/admin/migration/microsoft/admin-consent-url${query ? `?${query}` : ""}` ) }, }) } export function useMicrosoftAdminConsents() { return useQuery({ queryKey: ["admin", "migration", "microsoft", "admin-consents"], queryFn: () => apiClient.get<{ consents: MicrosoftAdminConsent[] }>( "/admin/migration/microsoft/admin-consents" ), }) } export function useMigrationInvite(token: string | null) { return useQuery({ queryKey: ["migration", "invite", token], enabled: Boolean(token), queryFn: () => apiClient.get( `/migration/invite?token=${encodeURIComponent(token ?? "")}` ), }) } export function useMigrationStatus() { return useQuery({ queryKey: ["migration", "status"], queryFn: () => apiClient.get("/migration/status"), }) } export function useClaimMigration() { const queryClient = useQueryClient() return useMutation({ mutationFn: (body: { token: string; password?: string }) => apiClient.post("/migration/claim", body), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["migration"] }) }, }) } export function useStartMigrationOAuth() { return useMutation({ mutationFn: (body: { provider: string; invite_token?: string; project_id?: string }) => apiClient.post<{ auth_url: string }>("/migration/oauth/start", body), }) } export function useCheckMailAddress(local: string, domain: string) { return useQuery({ queryKey: ["mail", "address-check", local, domain], enabled: local.length > 0 && domain.length > 0, queryFn: () => apiClient.get<{ available: boolean; reason?: string }>( `/mail/addresses/check?local=${encodeURIComponent(local)}&domain=${encodeURIComponent(domain)}` ), }) }