import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { workflowApi, type CandidateQuery } from './workflow' import type { CandidateSearchRow } from './workflow' // Query keys are namespaced so a bulk action can invalidate everything a candidate appears in with one call. // A bulk owner assignment changes the bench, the lists, and the desks at once, and a screen still showing // the old owner after the action reported success is how a recruiter stops trusting the button. const K = { search: (q: CandidateQuery) => ['candidates', 'search', q] as const, saved: ['candidates', 'saved-searches'] as const, lists: ['candidates', 'lists'] as const, listsFor: (id: string) => ['candidates', id, 'lists'] as const, docs: (id: string) => ['candidates', id, 'documents'] as const, messages: (id: string) => ['candidates', id, 'messages'] as const, work: (t: string, id: string) => ['work', t, id] as const, myClaims: ['work', 'mine'] as const, templates: ['message-templates'] as const, duplicates: ['candidates', 'duplicates'] as const, } export function useCandidateSearch(q: CandidateQuery, enabled = true) { return useQuery({ queryKey: K.search(q), queryFn: () => workflowApi.search(q), enabled, // A search is a question about right now. Keeping a previous answer while a new one loads stops the // table flashing empty between keystrokes, which reads as "no results" and is the single most common // way a search feels broken. placeholderData: (prev) => prev, }) } export function useSavedSearches() { return useQuery({ queryKey: K.saved, queryFn: workflowApi.savedSearches }) } export function useSaveSearch() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { name: string; criteria: CandidateQuery; visibility: 'private' | 'shared'; pinned: boolean }) => workflowApi.saveSearch(v.name, v.criteria, v.visibility, v.pinned), onSuccess: () => void qc.invalidateQueries({ queryKey: K.saved }), }) } export function useDeleteSavedSearch() { const qc = useQueryClient() return useMutation({ mutationFn: (id: string) => workflowApi.deleteSavedSearch(id), onSuccess: () => void qc.invalidateQueries({ queryKey: K.saved }), }) } export function useLists() { return useQuery({ queryKey: K.lists, queryFn: workflowApi.lists }) } export function useListsForCandidate(id: string) { return useQuery({ queryKey: K.listsFor(id), queryFn: () => workflowApi.listsForCandidate(id), enabled: !!id }) } export function useCreateList() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { name: string; purpose: string; description?: string }) => workflowApi.createList(v.name, v.purpose, v.description ?? ''), onSuccess: () => void qc.invalidateQueries({ queryKey: K.lists }), }) } export function useAddToList() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { listId: string; ids: string[]; note?: string }) => workflowApi.addToList(v.listId, v.ids, v.note ?? ''), onSuccess: () => { void qc.invalidateQueries({ queryKey: K.lists }) void qc.invalidateQueries({ queryKey: ['candidates'] }) }, }) } export function useRemoveFromList() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { listId: string; ids: string[] }) => workflowApi.removeFromList(v.listId, v.ids), onSuccess: () => { void qc.invalidateQueries({ queryKey: K.lists }) void qc.invalidateQueries({ queryKey: ['candidates'] }) }, }) } export function useBulkAssignOwner() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { ids: string[]; ownerUserId: string | null }) => workflowApi.bulkAssignOwner(v.ids, v.ownerUserId), onSuccess: () => void qc.invalidateQueries({ queryKey: ['candidates'] }), }) } // useWorkActivity RECORDS THE VISIT as it reads. That is deliberate: presence only means anything if opening // a record is what registers it, and asking a recruiter to press a button to say they are looking at // something is asking them to do the thing the feature exists to avoid. export function useWorkActivity(type: string, id: string, enabled = true) { return useQuery({ queryKey: K.work(type, id), queryFn: () => workflowApi.touch(type, id), enabled: enabled && !!id, // Presence goes stale quickly and is worth refetching while a record is open, so a colleague arriving // shows up without a reload. refetchInterval: 60_000, staleTime: 30_000, }) } export function useClaim() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { type: string; id: string; activity: string; note: string; steal?: boolean }) => workflowApi.claim(v.type, v.id, v.activity, v.note, v.steal), onSuccess: (_d, v) => { void qc.invalidateQueries({ queryKey: K.work(v.type, v.id) }) void qc.invalidateQueries({ queryKey: K.myClaims }) }, }) } export function useRelease() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { type: string; id: string }) => workflowApi.release(v.type, v.id), onSuccess: (_d, v) => { void qc.invalidateQueries({ queryKey: K.work(v.type, v.id) }) void qc.invalidateQueries({ queryKey: K.myClaims }) }, }) } export function useMyClaims() { return useQuery({ queryKey: K.myClaims, queryFn: workflowApi.myClaims }) } export function useCandidateDocuments(id: string) { return useQuery({ queryKey: K.docs(id), queryFn: () => workflowApi.documents(id), enabled: !!id }) } export function useSupersedeDocument(candidateId: string) { const qc = useQueryClient() return useMutation({ mutationFn: (docId: string) => workflowApi.supersedeDocument(docId), onSuccess: () => void qc.invalidateQueries({ queryKey: K.docs(candidateId) }), }) } export function useCandidateMessages(id: string) { return useQuery({ queryKey: K.messages(id), queryFn: () => workflowApi.candidateMessages(id), enabled: !!id }) } export function useTemplates() { return useQuery({ queryKey: K.templates, queryFn: workflowApi.templates }) } export function useSendMessage(candidateId: string) { const qc = useQueryClient() return useMutation({ mutationFn: workflowApi.sendMessage, onSuccess: () => { void qc.invalidateQueries({ queryKey: K.messages(candidateId) }) // A send also writes an activity row, so the record's history is stale too. void qc.invalidateQueries({ queryKey: ['recruiting', 'activities'] }) }, }) } export function useDuplicates() { return useQuery({ queryKey: K.duplicates, queryFn: workflowApi.duplicates }) } export function useMergeCandidates() { const qc = useQueryClient() return useMutation({ mutationFn: (v: { keepId: string; mergeId: string; reason: string }) => workflowApi.merge(v.keepId, v.mergeId, v.reason), // A merge moves rows across roughly eighteen tables. Rather than enumerate them, everything candidate // shaped is invalidated: it is one refetch on a rare action, against the alternative of a screen quietly // showing a person who no longer exists. onSuccess: () => { void qc.invalidateQueries({ queryKey: ['candidates'] }) void qc.invalidateQueries({ queryKey: ['work'] }) }, }) }