"use client" import { useCallback } from "react" import { useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" import { useUpdateContact } from "@/lib/api/hooks/use-contact-mutations" import { buildContactUpdatePayload } from "@/lib/api/adapters" import { findApiContactInCaches, replaceContactInBookCaches, resolveContactForUpdate, } from "@/lib/api/contact-book-cache" import { fetchContactByPath } from "@/lib/api/hooks/use-contact-queries" import { contactApiPath } from "@/lib/contacts/contact-api-path" import type { FullContact } from "@/lib/contacts/types" import type { ContactBulkEditField } from "@/lib/contacts/bulk-edit-fields" import { applyBulkFieldValue } from "@/lib/contacts/bulk-edit-fields" import { useBlockedSendersStore } from "@/lib/stores/blocked-senders-store" import { useNavStore } from "@/lib/stores/nav-store" import type { ContactLabelPresence } from "./contact-label-picker-block" function contactHasLabel(contact: FullContact, labelId: string): boolean { return contact.labels?.includes(labelId) ?? false } function mergeLabelOnContact( contact: FullContact, labelId: string, add: boolean, ): FullContact { const current = contact.labels ?? [] const next = add ? current.includes(labelId) ? current : [...current, labelId] : current.filter((id) => id !== labelId) return { ...contact, labels: next.length ? next : undefined, updatedAt: Date.now(), } } export function useContactBulkActions() { const queryClient = useQueryClient() const updateContact = useUpdateContact() const blockSenders = useBlockedSendersStore((s) => s.blockSenders) const labelRows = useNavStore((s) => s.labelRows) const addLabelRowFromSidebar = useNavStore((s) => s.addLabelRowFromSidebar) const resolveContactEtag = useCallback( async (contact: FullContact): Promise => { let resolved = resolveContactForUpdate(queryClient, contact) if (resolved.etag) return resolved const apiPath = contactApiPath(resolved) try { const fetched = await fetchContactByPath(apiPath) replaceContactInBookCaches(queryClient, apiPath, fetched) resolved = resolveContactForUpdate(queryClient, { ...resolved, etag: fetched.etag, path: fetched.path ?? resolved.path, }) } catch { return null } return resolved.etag ? resolved : null }, [queryClient], ) const persistContact = useCallback( async ( contact: FullContact, opts?: { bulkField?: ContactBulkEditField bulkValue?: string patchLabels?: boolean skipInvalidation?: boolean }, ) => { const resolved = await resolveContactEtag(contact) if (!resolved?.etag) return false const existing = findApiContactInCaches(queryClient, resolved) const payload = buildContactUpdatePayload(existing, resolved, { bulkField: opts?.bulkField, bulkValue: opts?.bulkValue, patchLabels: opts?.patchLabels, }) await updateContact.mutateAsync({ path: contactApiPath(resolved), etag: resolved.etag, contact: payload, skipInvalidation: opts?.skipInvalidation, }) return true }, [queryClient, resolveContactEtag, updateContact], ) const getLabelPresence = useCallback( (contacts: FullContact[], labelId: string): ContactLabelPresence => { if (contacts.length === 0) return "none" let n = 0 for (const contact of contacts) { if (contactHasLabel(contact, labelId)) n++ } if (n === 0) return "none" if (n === contacts.length) return "all" return "some" }, [], ) const runSequentialUpdates = useCallback( async ( contacts: FullContact[], buildNext: (contact: FullContact) => FullContact, opts?: { bulkField?: ContactBulkEditField bulkValue?: string patchLabels?: boolean }, ) => { let skipped = 0 let failed = 0 let updated = 0 for (const contact of contacts) { const next = buildNext(resolveContactForUpdate(queryClient, contact)) try { const ok = await persistContact(next, { ...opts, skipInvalidation: true, }) if (ok) updated++ else skipped++ } catch { failed++ } } await queryClient.invalidateQueries({ queryKey: ["contacts"] }) return { skipped, failed, updated } }, [persistContact, queryClient], ) const toggleLabelOnContacts = useCallback( async (contacts: FullContact[], labelId: string) => { if (contacts.length === 0) return const presence = getLabelPresence(contacts, labelId) const shouldAdd = presence !== "all" const targets = contacts.filter((contact) => { const has = contactHasLabel(contact, labelId) return shouldAdd ? !has : has }) const { skipped, failed, updated } = await runSequentialUpdates( targets, (contact) => mergeLabelOnContact(contact, labelId, shouldAdd), { patchLabels: true }, ) if (updated === 0 && skipped === 0 && failed === 0) return if (failed > 0) { toast.error( `${failed} mise${failed > 1 ? "s" : ""} à jour échouée${failed > 1 ? "s" : ""}`, ) } if (skipped > 0) { toast.warning( `${skipped} contact${skipped > 1 ? "s" : ""} ignoré${skipped > 1 ? "s" : ""} (version inconnue)`, ) } }, [getLabelPresence, runSequentialUpdates], ) const createAndApplyLabel = useCallback( async (contacts: FullContact[], labelText: string) => { addLabelRowFromSidebar(labelText) const row = useNavStore .getState() .labelRows.find( (r) => r.label.toLowerCase() === labelText.trim().toLowerCase(), ) if (row) await toggleLabelOnContacts(contacts, row.id) }, [addLabelRowFromSidebar, toggleLabelOnContacts], ) const blockContacts = useCallback( (contacts: FullContact[]) => { const emails = contacts .flatMap((c) => c.emails.map((e) => e.value)) .filter(Boolean) if (emails.length === 0) { toast.error("Aucune adresse e-mail à bloquer dans la sélection") return } blockSenders(emails) toast.success( `${emails.length} adresse${emails.length > 1 ? "s" : ""} bloquée${emails.length > 1 ? "s" : ""}`, ) }, [blockSenders], ) const applyBulkField = useCallback( async (contacts: FullContact[], field: ContactBulkEditField, value: string) => { const toUpdate = contacts.map((c) => applyBulkFieldValue(c, field, value)) const { skipped, failed, updated } = await runSequentialUpdates( toUpdate, (contact) => contact, { bulkField: field, bulkValue: value }, ) if (updated > 0) { toast.success( `${updated} contact${updated > 1 ? "s" : ""} mis à jour`, ) } if (failed > 0) { toast.error( `${failed} mise${failed > 1 ? "s" : ""} à jour échouée${failed > 1 ? "s" : ""}`, ) } if (skipped > 0) { toast.warning( `${skipped} contact${skipped > 1 ? "s" : ""} ignoré${skipped > 1 ? "s" : ""} (version inconnue)`, ) } }, [runSequentialUpdates], ) const resolveLabelVisualById = useCallback( (labelId: string) => { const row = labelRows.find((r) => r.id === labelId) if (row?.icon) return { kind: "iconify" as const, icon: row.icon } if (row?.color) return { kind: "dot" as const, colorClass: row.color } return { kind: "dot" as const, colorClass: "bg-gray-400" } }, [labelRows], ) return { getLabelPresence, toggleLabelOnContacts, createAndApplyLabel, blockContacts, applyBulkField, resolveLabelVisualById, isUpdating: updateContact.isPending, } }