ultisuite-client/lib/contacts/contacts-store.ts
R3D347HR4Y c87670e90f
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
feat(api): offline-first mail sync w/ TanStack Query
Move mail, compose, contacts, and accounts off mocks onto REST + WS.
Add client, auth store, IDB-backed query cache, offline queue, and
sync bar; hybrid Zustand for UI-only state. Settings still local until
backend has preferences API.
2026-05-23 00:04:28 +02:00

167 lines
4.5 KiB
TypeScript

"use client"
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { debouncedPersistJSONStorage } from "@/lib/stores/debounced-json-storage"
import type { FullContact } from "./types"
type ContactsView = "list" | "view" | "create" | "edit"
export type ContactCreateDraft = {
firstName?: string
lastName?: string
emails?: { value: string; label: string }[]
}
export interface DeletedContact {
contact: FullContact
deletedAt: number
reason: string
}
interface ContactsState {
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
softDeleteContact: (contact: FullContact, reason?: string) => void
restoreContact: (id: string) => void
emptyTrash: () => void
ignoreMergePair: (idA: string, idB: string) => void
}
export type ContactsStore = ContactsState & ContactsActions
export const useContactsStore = create<ContactsStore>()(
persist(
(set) => ({
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: "" }),
softDeleteContact: (contact, reason = "Supprimé manuellement") =>
set((s) => ({
deletedContacts: [
...s.deletedContacts,
{ contact, deletedAt: Date.now(), reason },
],
activeContactId: s.activeContactId === contact.id ? null : s.activeContactId,
view: s.activeContactId === contact.id ? "list" : s.view,
})),
restoreContact: (id) =>
set((s) => {
const entry = s.deletedContacts.find((d) => d.contact.id === id)
if (!entry) return s
return {
deletedContacts: s.deletedContacts.filter((d) => d.contact.id !== id),
}
}),
emptyTrash: () => set({ deletedContacts: [] }),
ignoreMergePair: (idA, idB) =>
set((s) => {
const key = [idA, idB].sort().join("::")
if (s.ignoredMergePairs.includes(key)) return s
return { ignoredMergePairs: [...s.ignoredMergePairs, key] }
}),
}),
{
name: "contacts-store",
storage: debouncedPersistJSONStorage,
partialize: (state) => ({
deletedContacts: state.deletedContacts,
ignoredMergePairs: state.ignoredMergePairs,
}),
}
)
)