import { authFetch } from '../../../../utils/authFetch'; export type CandidateItem = { userId: number; email: string; userType: 'personal' | 'company'; name: string; }; export type UserCandidatesResponse = { success: boolean; data?: { q: string | null; type: 'all' | 'personal' | 'company'; rootUserId: number | null; limit: number; offset: number; total: number; items: CandidateItem[]; }; message?: string; }; export type UserCandidatesData = { q: string | null; type: 'all' | 'personal' | 'company'; rootUserId: number | null; limit: number; offset: number; total: number; items: CandidateItem[]; _debug?: { endpoint: string; query: Record; combo: string; }; }; export type GetUserCandidatesParams = { q: string; type?: 'all' | 'personal' | 'company'; rootUserId?: number; matrixId?: string | number; topNodeEmail?: string; limit?: number; offset?: number; }; export async function getUserCandidates(params: GetUserCandidatesParams): Promise { const { q, type = 'all', rootUserId, matrixId, topNodeEmail, limit = 20, offset = 0 } = params; const base = (process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(/\/+$/, ''); if (!base) { console.warn('[getUserCandidates] NEXT_PUBLIC_API_BASE_URL not set. Falling back to same-origin.'); } const qTrimmed = q.trim(); console.info('[getUserCandidates] Building candidate request', { base, q: qTrimmed, typeSent: type !== 'all' ? type : undefined, identifiers: { rootUserId, matrixId, topNodeEmail }, pagination: { limit, offset } }); // Build identifier combinations: all -> root-only -> matrix-only -> email-only const combos: Array<{ label: string; apply: (qs: URLSearchParams) => void }> = []; const hasRoot = typeof rootUserId === 'number' && rootUserId > 0; const hasMatrix = !!matrixId; const hasEmail = !!topNodeEmail; if (hasRoot || hasMatrix || hasEmail) { combos.push({ label: 'all-identifiers', apply: (qs) => { if (hasRoot) qs.set('rootUserId', String(rootUserId)); if (hasMatrix) qs.set('matrixId', String(matrixId)); if (hasEmail) qs.set('topNodeEmail', String(topNodeEmail)); } }); } if (hasRoot) combos.push({ label: 'root-only', apply: (qs) => { qs.set('rootUserId', String(rootUserId)); } }); if (hasMatrix) combos.push({ label: 'matrix-only', apply: (qs) => { qs.set('matrixId', String(matrixId)); } }); if (hasEmail) combos.push({ label: 'email-only', apply: (qs) => { qs.set('topNodeEmail', String(topNodeEmail)); } }); if (combos.length === 0) combos.push({ label: 'no-identifiers', apply: () => {} }); const endpointVariants = [ (qs: string) => `${base}/api/admin/matrix/users/candidates?${qs}`, (qs: string) => `${base}/api/admin/matrix/user-candidates?${qs}` ]; console.debug('[getUserCandidates] Endpoint variants', endpointVariants.map(f => f('...'))); let lastError: any = null; let lastZeroData: UserCandidatesData | null = null; // Try each identifier combo against both endpoint variants for (const combo of combos) { const qs = new URLSearchParams(); qs.set('q', qTrimmed); qs.set('limit', String(limit)); qs.set('offset', String(offset)); if (type !== 'all') qs.set('type', type); combo.apply(qs); const qsObj = Object.fromEntries(qs.entries()); console.debug('[getUserCandidates] Final query params', { combo: combo.label, qs: qsObj }); for (let i = 0; i < endpointVariants.length; i++) { const url = endpointVariants[i](qs.toString()); const fetchOpts = { method: 'GET', headers: { Accept: 'application/json' } as const }; console.info('[getUserCandidates] REQUEST GET', { url, attempt: i + 1, combo: combo.label, identifiers: { rootUserId, matrixId, topNodeEmail }, params: { q: qTrimmed, type: type !== 'all' ? type : undefined, limit, offset }, fetchOpts }); const t0 = performance.now(); const res = await authFetch(url, fetchOpts); const t1 = performance.now(); const ct = res.headers.get('content-type') || ''; console.debug('[getUserCandidates] Response meta', { status: res.status, ok: res.ok, durationMs: Math.round(t1 - t0), contentType: ct }); // Preview raw body (first 300 chars) let rawPreview = ''; try { rawPreview = await res.clone().text(); } catch {} if (rawPreview) { console.trace('[getUserCandidates] Raw body preview (trimmed)', rawPreview.slice(0, 300)); } if (res.status === 404 && i < endpointVariants.length - 1) { try { const preview = ct.includes('application/json') ? await res.json() : await res.text(); console.warn('[getUserCandidates] 404 on endpoint variant, trying fallback', { tried: url, combo: combo.label, preview: typeof preview === 'string' ? preview.slice(0, 200) : preview }); } catch {} continue; } if (!ct.includes('application/json')) { const text = await res.text().catch(() => ''); console.error('[getUserCandidates] Non-JSON response', { status: res.status, bodyPreview: text.slice(0, 500) }); lastError = new Error(`Request failed: ${res.status}`); break; } const json: UserCandidatesResponse = await res.json().catch(() => ({ success: false, message: 'Invalid JSON' } as any)); console.debug('[getUserCandidates] Parsed JSON', { success: json?.success, message: json?.message, dataMeta: json?.data && { q: json.data.q, type: json.data.type, total: json.data.total, itemsCount: json.data.items?.length } }); if (!res.ok || !json?.success) { console.error('[getUserCandidates] Backend reported failure', { status: res.status, successFlag: json?.success, message: json?.message }); lastError = new Error(json?.message || `Request failed: ${res.status}`); break; } const dataWithDebug: UserCandidatesData = { ...json.data!, _debug: { endpoint: url, query: qsObj, combo: combo.label } }; if ((dataWithDebug.total || 0) > 0) { console.info('[getUserCandidates] Success (non-empty)', { total: dataWithDebug.total, itemsCount: dataWithDebug.items.length, combo: combo.label, endpoint: url }); return dataWithDebug; } // Keep last zero result but continue trying other combos/endpoints lastZeroData = dataWithDebug; console.info('[getUserCandidates] Success (empty)', { combo: combo.label, endpoint: url }); } if (lastError) break; // stop on hard error } if (lastError) { console.error('[getUserCandidates] Exhausted endpoint variants with error', { lastError: lastError?.message }); throw lastError; } // Return the last empty response (with _debug info) if everything was empty if (lastZeroData) { console.warn('[getUserCandidates] All combos returned empty results', { lastCombo: lastZeroData._debug }); return lastZeroData; } throw new Error('Request failed'); }