SPB Git

spb/job-ka

Public
5.8 KB · 194 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  is_direct?: boolean;47  company_logo?: string | null;48  language?: string | null;       // fr | en | bilingue49  apply_url?: string | null;      // candidature directe (≠ url de la fiche)50  title_clean?: string | null;    // titre d'affichage normalisé51}5253export interface JobList {54  total: number;55  count: number;56  jobs: Job[];57}5859export interface Facets {60  cities: string[];61  regions: { region: string; n: number }[];62  languages: { language: string; n: number }[];63  categories: { category: string; n: number }[];64  employers: { employer: string; n: number }[];65  work_modes: string[];66  employment_types: string[];67  sources: { source: string; n: number }[];68}6970export interface Source {71  id: string;72  name: string;73  url: string;74  careers_url: string;75  sectors: string[];76  connector: string;77  status: string;78  region: string;79  active_jobs: number;80  last_sync: number | null;81}8283export interface Stats {84  total: number;85  employers: number;86  sources: number;87  with_salary: number;88  avg_salary_year: number | null;89  remote: number;90  geocoded: number;91  direct_jobs?: number;92  direct_employers?: number;93  direct_sources?: number;94  portal_jobs?: number;95  duplicates_hidden?: number;96  top_cities: { city: string; n: number }[];97  categories: { category: string; n: number }[];98  recent_syncs: {99    source: string; ts: number; found: number; added: number;100    updated: number; removed: number; ok: number; message: string;101  }[];102}103104async function get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T> {105  const url = new URL(path, window.location.origin);106  if (params) {107    for (const [k, v] of Object.entries(params)) {108      if (v !== undefined && v !== "") url.searchParams.set(k, String(v));109    }110  }111  const ctl = new AbortController();112  const timer = setTimeout(() => ctl.abort(), 20000);113  try {114    const resp = await fetch(url.toString(), { signal: ctl.signal });115    if (!resp.ok) throw new Error(`API ${resp.status} : ${path}`);116    return (await resp.json()) as T;117  } finally {118    clearTimeout(timer);119  }120}121122export interface JobFilters {123  q?: string;124  city?: string;125  region?: string;126  category?: string;127  work_mode?: string;128  employment_type?: string;129  language?: string;130  salary_min?: number;131  with_salary?: number;132  sort?: string;133}134135export const fetchJobs = (filters: JobFilters, limit = 30, offset = 0) =>136  get<JobList>("/api/jobs", { ...filters, limit, offset });137138export const fetchJob = (uid: string) => get<Job>(`/api/jobs/${uid}`);139140export const fetchFacets = () => get<Facets>("/api/facets");141142export const fetchSources = () => get<{ sources: Source[] }>("/api/sources");143144export const fetchStats = () => get<Stats>("/api/stats");145146export const geojsonUrl = (filters: JobFilters) => {147  const url = new URL("/api/jobs.geojson", window.location.origin);148  for (const [k, v] of Object.entries(filters)) {149    if (v !== undefined && v !== "") url.searchParams.set(k, String(v));150  }151  return url.toString();152};153154// --- présentation ------------------------------------------------------------155156const UNIT_FR: Record<string, string> = {157  hour: "h", day: "jour", week: "sem.", biweek: "2 sem.", month: "mois", year: "an",158};159160export function formatSalary(job: Pick<Job, "salary_min" | "salary_max" | "salary_unit">): string | null {161  if (job.salary_min == null || !job.salary_unit) return null;162  const fmt = (n: number) =>163    n >= 1000 ? n.toLocaleString("fr-CA", { maximumFractionDigits: 0 })164              : n.toLocaleString("fr-CA", { minimumFractionDigits: 2, maximumFractionDigits: 2 });165  const unit = UNIT_FR[job.salary_unit] ?? job.salary_unit;166  if (job.salary_max != null && job.salary_max > job.salary_min) {167    return `${fmt(job.salary_min)} $ à ${fmt(job.salary_max)} $ / ${unit}`;168  }169  return `${fmt(job.salary_min)} $ / ${unit}`;170}171172export const MODE_FR: Record<string, string> = {173  presentiel: "Présentiel", hybride: "Hybride", teletravail: "Télétravail",174};175176export const TYPE_FR: Record<string, string> = {177  temps_plein: "Temps plein", temps_partiel: "Temps partiel",178  contractuel: "Contractuel", stage: "Stage", saisonnier: "Saisonnier",179};180181export const LANG_FR: Record<string, string> = {182  fr: "Français", en: "Anglais", bilingue: "Bilingue",183};184185/** Titre d'affichage : version normalisée si disponible, sinon le titre source. */186export const displayTitle = (job: Pick<Job, "title" | "title_clean">) =>187  job.title_clean || job.title;188189export function formatDate(iso: string | null): string {190  if (!iso) return "";191  const d = new Date(`${iso}T12:00:00`);192  return d.toLocaleDateString("fr-CA", { day: "numeric", month: "long", year: "numeric" });193}194
Files · 6bec01007ca45174458fd0ecc0feefebe45339f4
Menu

Theme