Files
srbhr--resume-matcher/apps/frontend/lib/api/tracker.ts
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

160 lines
4.8 KiB
TypeScript

import { apiFetch, apiPost, apiPatch, apiDelete } from './client';
// The seven stable Kanban columns (keys are decoupled from i18n labels).
export type ApplicationStatus =
| 'saved'
| 'applied'
| 'no_response'
| 'response'
| 'interview'
| 'accepted'
| 'rejected';
export const APPLICATION_STATUS_ORDER: ApplicationStatus[] = [
'saved',
'applied',
'no_response',
'response',
'interview',
'accepted',
'rejected',
];
export interface Application {
application_id: string;
job_id: string;
resume_id: string;
master_resume_id: string | null;
status: ApplicationStatus;
company: string | null;
role: string | null;
applied_at: string | null;
notes: string | null;
position: number;
created_at: string;
updated_at: string;
}
export interface ApplicationDetail extends Application {
job_content: string | null;
// The applied/tailored resume record (null when it has been deleted).
resume: Record<string, unknown> | null;
}
export type ApplicationColumns = Record<ApplicationStatus, Application[]>;
export interface ApplicationListResponse {
columns: ApplicationColumns;
}
export interface ManualApplicationCreate {
resume_id: string;
job_description: string;
company?: string;
role?: string;
status?: ApplicationStatus;
notes?: string;
}
export interface ApplicationUpdate {
status?: ApplicationStatus;
position?: number;
notes?: string;
company?: string;
role?: string;
applied_at?: string;
}
export interface ApplicationActionResponse {
message: string;
affected: number;
}
// FastAPI returns `detail` as a string for HTTPException but as an array of
// `{ msg, loc, ... }` objects for validation errors — coerce both to a string
// so error messages never render as "[object Object]".
function extractDetail(data: unknown): string | null {
if (!data || typeof data !== 'object') return null;
const detail = (data as { detail?: unknown }).detail;
if (typeof detail === 'string') return detail;
if (Array.isArray(detail)) {
const messages = detail
.map((d) =>
d && typeof d === 'object' && 'msg' in d ? String((d as { msg: unknown }).msg) : null
)
.filter((m): m is string => Boolean(m));
if (messages.length > 0) return messages.join('; ');
}
// A dict detail (e.g. HTTPException(detail={...})) — stringify so it reads as
// something rather than "[object Object]".
if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
try {
return JSON.stringify(detail);
} catch {
return null;
}
}
return null;
}
async function asJson<T>(res: Response, fallback: string): Promise<T> {
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(extractDetail(data) || `${fallback} (status ${res.status}).`);
}
return res.json() as Promise<T>;
}
// List all applications grouped by status column.
export async function listApplications(): Promise<ApplicationListResponse> {
const res = await apiFetch('/applications', { credentials: 'include' });
return asJson<ApplicationListResponse>(res, 'Failed to load applications');
}
// Manually add a card from a pasted job description.
export async function createApplication(payload: ManualApplicationCreate): Promise<Application> {
const res = await apiPost('/applications', payload);
return asJson<Application>(res, 'Failed to create application');
}
// Fetch a card with its embedded JD + applied resume (for the modal).
export async function getApplicationDetail(id: string): Promise<ApplicationDetail> {
const res = await apiFetch(`/applications/${id}`, { credentials: 'include' });
return asJson<ApplicationDetail>(res, 'Failed to load application');
}
// Update one card (status/position/notes/company/role/applied_at).
export async function updateApplication(
id: string,
payload: ApplicationUpdate
): Promise<Application> {
const res = await apiPatch(`/applications/${id}`, payload);
return asJson<Application>(res, 'Failed to update application');
}
// Move many cards to one column.
export async function bulkUpdateStatus(
applicationIds: string[],
status: ApplicationStatus
): Promise<ApplicationActionResponse> {
const res = await apiPatch('/applications/bulk', {
application_ids: applicationIds,
status,
});
return asJson<ApplicationActionResponse>(res, 'Failed to move applications');
}
// Delete one card.
export async function deleteApplication(id: string): Promise<void> {
const res = await apiDelete(`/applications/${id}`);
await asJson<ApplicationActionResponse>(res, 'Failed to delete application');
}
// Delete many cards.
export async function bulkDeleteApplications(
applicationIds: string[]
): Promise<ApplicationActionResponse> {
const res = await apiPost('/applications/bulk-delete', { application_ids: applicationIds });
return asJson<ApplicationActionResponse>(res, 'Failed to delete applications');
}