SPB Git

spb/job-ka

Public
5.0 KB · 172 lines typescript
1/**2 * =============================================================================3 * Job·Ka — Groupe KA4 * Auteur  : Simon-Pierre Boucher5 * Contact : contact@spboucher.ai6 * Fichier : frontend/src/api.ts7 * Rôle    : Types + client API typé (timeout, erreurs) — miroir de jobka/web.py8 * Créé    : 2026-08-17   Modifié : 2026-08-179 * =============================================================================10 */1112export interface Job {13  uid: string;14  source: string;15  external_id: string;16  url: string;17  employer: string;18  title: string;19  description?: string;20  address: string;21  city: string;22  region: string;23  postal_code: string;24  location_label: string;25  work_mode: string | null;26  employment_type: string | null;27  salary_min: number | null;28  salary_max: number | null;29  salary_unit: string | null;30  salary_label: string;31  salary_year_min: number | null;32  salary_year_max: number | null;33  salary_hour_min: number | null;34  salary_hour_max: number | null;35  benefits: string[];36  requirements: Record<string, unknown>;37  date_posted: string | null;38  date_deadline: string | null;39  category: string;40  ats: string;41  details: Record<string, unknown>;42  lat: number | null;43  lng: number | null;44  active: number;45  dup_sources?: string[];46}4748export interface JobList {49  total: number;50  count: number;51  jobs: Job[];52}5354export interface Facets {55  cities: string[];56  categories: { category: string; n: number }[];57  employers: { employer: string; n: number }[];58  work_modes: string[];59  employment_types: string[];60  sources: { source: string; n: number }[];61}6263export interface Source {64  id: string;65  name: string;66  url: string;67  careers_url: string;68  sectors: string[];69  connector: string;70  status: string;71  region: string;72  active_jobs: number;73  last_sync: number | null;74}7576export interface Stats {77  total: number;78  employers: number;79  sources: number;80  with_salary: number;81  avg_salary_year: number | null;82  remote: number;83  geocoded: number;84  top_cities: { city: string; n: number }[];85  categories: { category: string; n: number }[];86  recent_syncs: {87    source: string; ts: number; found: number; added: number;88    updated: number; removed: number; ok: number; message: string;89  }[];90}9192async function get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {93  const url = new URL(path, window.location.origin);94  if (params) {95    for (const [k, v] of Object.entries(params)) {96      if (v !== undefined && v !== "") url.searchParams.set(k, String(v));97    }98  }99  const ctl = new AbortController();100  const timer = setTimeout(() => ctl.abort(), 20000);101  try {102    const resp = await fetch(url.toString(), { signal: ctl.signal });103    if (!resp.ok) throw new Error(`API ${resp.status} : ${path}`);104    return (await resp.json()) as T;105  } finally {106    clearTimeout(timer);107  }108}109110export interface JobFilters {111  q?: string;112  city?: string;113  category?: string;114  work_mode?: string;115  employment_type?: string;116  salary_min?: number;117  with_salary?: number;118  sort?: string;119}120121export const fetchJobs = (filters: JobFilters, limit = 30, offset = 0) =>122  get<JobList>("/api/jobs", { ...filters, limit, offset });123124export const fetchJob = (uid: string) => get<Job>(`/api/jobs/${uid}`);125126export const fetchFacets = () => get<Facets>("/api/facets");127128export const fetchSources = () => get<{ sources: Source[] }>("/api/sources");129130export const fetchStats = () => get<Stats>("/api/stats");131132export const geojsonUrl = (filters: JobFilters) => {133  const url = new URL("/api/jobs.geojson", window.location.origin);134  for (const [k, v] of Object.entries(filters)) {135    if (v !== undefined && v !== "") url.searchParams.set(k, String(v));136  }137  return url.toString();138};139140// --- présentation ------------------------------------------------------------141142const UNIT_FR: Record<string, string> = {143  hour: "h", day: "jour", week: "sem.", biweek: "2 sem.", month: "mois", year: "an",144};145146export function formatSalary(job: Pick<Job, "salary_min" | "salary_max" | "salary_unit">): string | null {147  if (job.salary_min == null || !job.salary_unit) return null;148  const fmt = (n: number) =>149    n >= 1000 ? n.toLocaleString("fr-CA", { maximumFractionDigits: 0 })150              : n.toLocaleString("fr-CA", { minimumFractionDigits: 2, maximumFractionDigits: 2 });151  const unit = UNIT_FR[job.salary_unit] ?? job.salary_unit;152  if (job.salary_max != null && job.salary_max > job.salary_min) {153    return `${fmt(job.salary_min)} $ à ${fmt(job.salary_max)} $ / ${unit}`;154  }155  return `${fmt(job.salary_min)} $ / ${unit}`;156}157158export const MODE_FR: Record<string, string> = {159  presentiel: "Présentiel", hybride: "Hybride", teletravail: "Télétravail",160};161162export const TYPE_FR: Record<string, string> = {163  temps_plein: "Temps plein", temps_partiel: "Temps partiel",164  contractuel: "Contractuel", stage: "Stage", saisonnier: "Saisonnier",165};166167export function formatDate(iso: string | null): string {168  if (!iso) return "";169  const d = new Date(`${iso}T12:00:00`);170  return d.toLocaleDateString("fr-CA", { day: "numeric", month: "long", year: "numeric" });171}172
Files · 1fc94acf55099a4e4db9dfaaffbba3f871ff8727
Menu

Theme