225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
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<string, string>;
|
|
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<UserCandidatesData> {
|
|
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');
|
|
}
|