"use client" import { create } from "zustand" import { persist } from "zustand/middleware" import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage" import { findDuplicatePairs, mergePairKey, normalizePhone, type DuplicateMatchReason, } from "./duplicate-detection" import { MOCK_FULL_CONTACTS } from "./mock-data" import type { FullContact } from "./types" type ContactsView = "list" | "view" | "create" | "edit" /** Prefill for "Nouveau contact" opened from hover card / elsewhere. */ export type ContactCreateDraft = { firstName?: string lastName?: string emails?: { value: string; label: string }[] } export interface DeletedContact { contact: FullContact deletedAt: number reason: string } export interface MergeSuggestion { contactA: FullContact contactB: FullContact reason: DuplicateMatchReason } export interface CoordinateSuggestion { contact: FullContact suggestedField: string suggestedValue: string } interface ContactsState { contacts: FullContact[] deletedContacts: DeletedContact[] ignoredMergePairs: string[] panelOpen: boolean view: ContactsView activeContactId: string | null searchQuery: string searchMode: boolean createDraft: ContactCreateDraft | null } interface ContactsActions { togglePanel: () => void openPanel: () => void closePanel: () => void openContactDetail: (contactId: string) => void openCreateContact: (draft?: ContactCreateDraft | null) => void clearCreateDraft: () => void setView: (view: ContactsView, activeContactId?: string | null) => void showContactsList: () => void setSearchQuery: (q: string) => void setSearchMode: (active: boolean) => void addContact: ( contact: Omit ) => string addContacts: ( contacts: Omit[] ) => number updateContact: (id: string, patch: Partial) => void deleteContact: (id: string) => void softDeleteContact: (id: string, reason?: string) => void restoreContact: (id: string) => void emptyTrash: () => void mergeContacts: (keepId: string, mergeId: string) => void ignoreMergePair: (idA: string, idB: string) => void getMergeSuggestions: () => MergeSuggestion[] getCoordinateSuggestions: () => CoordinateSuggestion[] } export type ContactsStore = ContactsState & ContactsActions function computeCoordinateSuggestions(contacts: FullContact[]): CoordinateSuggestion[] { const suggestions: CoordinateSuggestion[] = [] const emailDomains = new Map() for (const c of contacts) { if (c.company) { for (const e of c.emails) { const domain = e.value.split("@")[1]?.toLowerCase() if (domain && !domain.includes("gmail") && !domain.includes("outlook") && !domain.includes("yahoo") && !domain.includes("proton")) { emailDomains.set(domain, { company: c.company, jobTitle: c.jobTitle }) } } } } for (const c of contacts) { if (c.company) continue for (const e of c.emails) { const domain = e.value.split("@")[1]?.toLowerCase() if (domain && emailDomains.has(domain)) { const info = emailDomains.get(domain)! if (info.company) { suggestions.push({ contact: c, suggestedField: "company", suggestedValue: info.company }) break } } } if (suggestions.length >= 20) break } return suggestions } export const useContactsStore = create()( persist( (set, get) => ({ contacts: MOCK_FULL_CONTACTS, deletedContacts: [], ignoredMergePairs: [], panelOpen: false, view: "list", activeContactId: null, searchQuery: "", searchMode: false, createDraft: null, togglePanel: () => set((s) => s.panelOpen ? { panelOpen: false, view: "list", activeContactId: null, searchQuery: "", searchMode: false, createDraft: null, } : { panelOpen: true } ), openPanel: () => set({ panelOpen: true }), closePanel: () => set({ panelOpen: false, view: "list", activeContactId: null, searchQuery: "", searchMode: false, createDraft: null, }), openContactDetail: (contactId) => set({ panelOpen: true, view: "view", activeContactId: contactId, searchQuery: "", searchMode: false, createDraft: null, }), openCreateContact: (draft = null) => set({ panelOpen: true, view: "create", activeContactId: null, searchQuery: "", searchMode: false, createDraft: draft, }), clearCreateDraft: () => set({ createDraft: null }), setView: (view, activeContactId = null) => set({ view, activeContactId, createDraft: null }), showContactsList: () => set({ view: "list", activeContactId: null, searchQuery: "", searchMode: false, createDraft: null, }), setSearchQuery: (searchQuery) => set({ searchQuery }), setSearchMode: (searchMode) => set(searchMode ? { searchMode } : { searchMode, searchQuery: "" }), addContact: (contact) => { const id = `contact-${crypto.randomUUID()}` const now = Date.now() const full: FullContact = { ...contact, id, createdAt: now, updatedAt: now } set((s) => ({ contacts: [...s.contacts, full] })) return id }, addContacts: (incoming) => { if (incoming.length === 0) return 0 const now = Date.now() const added = incoming.map((contact) => ({ ...contact, id: `contact-${crypto.randomUUID()}`, createdAt: now, updatedAt: now, })) set((s) => ({ contacts: [...s.contacts, ...added] })) return added.length }, updateContact: (id, patch) => set((s) => ({ contacts: s.contacts.map((c) => c.id === id ? { ...c, ...patch, updatedAt: Date.now() } : c ), })), deleteContact: (id) => set((s) => ({ contacts: s.contacts.filter((c) => c.id !== id), activeContactId: s.activeContactId === id ? null : s.activeContactId, view: s.activeContactId === id ? "list" : s.view, })), softDeleteContact: (id, reason = "Supprimé manuellement") => set((s) => { const contact = s.contacts.find((c) => c.id === id) if (!contact) return s return { contacts: s.contacts.filter((c) => c.id !== id), deletedContacts: [ ...s.deletedContacts, { contact, deletedAt: Date.now(), reason }, ], activeContactId: s.activeContactId === id ? null : s.activeContactId, view: s.activeContactId === id ? "list" : s.view, } }), restoreContact: (id) => set((s) => { const entry = s.deletedContacts.find((d) => d.contact.id === id) if (!entry) return s return { contacts: [...s.contacts, entry.contact], deletedContacts: s.deletedContacts.filter((d) => d.contact.id !== id), } }), emptyTrash: () => set({ deletedContacts: [] }), mergeContacts: (keepId, mergeId) => set((s) => { const keep = s.contacts.find((c) => c.id === keepId) const merge = s.contacts.find((c) => c.id === mergeId) if (!keep || !merge) return s const mergedEmails = [...keep.emails] for (const e of merge.emails) { if (!mergedEmails.some((me) => me.value.toLowerCase() === e.value.toLowerCase())) { mergedEmails.push(e) } } const mergedPhones = [...keep.phones] for (const p of merge.phones) { const norm = normalizePhone(p.value) if ( !mergedPhones.some( (mp) => normalizePhone(mp.value) === norm && norm.length > 0 ) ) { mergedPhones.push(p) } } const mergedLabels = [ ...new Set([...(keep.labels ?? []), ...(merge.labels ?? [])]), ] const merged: FullContact = { ...keep, firstName: keep.firstName || merge.firstName, lastName: keep.lastName || merge.lastName, emails: mergedEmails, phones: mergedPhones, labels: mergedLabels.length ? mergedLabels : undefined, company: keep.company || merge.company, jobTitle: keep.jobTitle || merge.jobTitle, department: keep.department || merge.department, birthday: keep.birthday || merge.birthday, avatarUrl: keep.avatarUrl || merge.avatarUrl, notes: [keep.notes, merge.notes].filter(Boolean).join("\n") || undefined, updatedAt: Date.now(), } const pairKey = mergePairKey(keepId, mergeId) return { contacts: s.contacts .filter((c) => c.id !== mergeId) .map((c) => (c.id === keepId ? merged : c)), ignoredMergePairs: s.ignoredMergePairs.includes(pairKey) ? s.ignoredMergePairs : [...s.ignoredMergePairs, pairKey], } }), ignoreMergePair: (idA, idB) => set((s) => { const key = mergePairKey(idA, idB) if (s.ignoredMergePairs.includes(key)) return s return { ignoredMergePairs: [...s.ignoredMergePairs, key] } }), getMergeSuggestions: () => { const s = get() const ignored = new Set(s.ignoredMergePairs) return findDuplicatePairs(s.contacts, ignored).map((p) => ({ contactA: p.contactA, contactB: p.contactB, reason: p.reason, })) }, getCoordinateSuggestions: () => computeCoordinateSuggestions(get().contacts), }), { name: "contacts-store", storage: debouncedPersistJSONStorage, partialize: (state) => ({ contacts: state.contacts, deletedContacts: state.deletedContacts, ignoredMergePairs: state.ignoredMergePairs, }), } ) )