import { getToken } from "./auth"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8521"; export interface ApiUser { id: string; name: string; email: string; identifier: string; phone?: string; role: string; status: string; created_at: string; } export interface ApiCompany { id: string; name: string; slug: string; email?: string; phone?: string; website?: string; address?: string; active: boolean; verified: boolean; created_at: string; } async function apiRequest( endpoint: string, options: RequestInit = {} ): Promise { const token = getToken(); // Sanitize API_URL: remove trailing slash let baseUrl = API_URL.replace(/\/+$/, ""); // Sanitize endpoint: ensure leading slash let cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; // Detect and fix double prefixing of /api/v1 // Case 1: BaseURL ends with /api/v1 AND endpoint starts with /api/v1 if (baseUrl.endsWith("/api/v1") && cleanEndpoint.startsWith("/api/v1")) { cleanEndpoint = cleanEndpoint.replace("/api/v1", ""); } // Case 2: Double /api/v1 inside endpoint itself (if passed incorrectly) if (cleanEndpoint.includes("/api/v1/api/v1")) { cleanEndpoint = cleanEndpoint.replace("/api/v1/api/v1", "/api/v1"); } const url = `${baseUrl}${cleanEndpoint}`; console.log(`[API Request] ${url}`); // Debug log const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); let res: Response; try { res = await fetch(url, { ...options, signal: controller.signal, headers: { "Content-Type": "application/json", ...(token && { Authorization: `Bearer ${token}` }), ...options.headers, }, }); } finally { clearTimeout(timeout); } if (!res.ok) { const error = await res.text(); throw new Error(error || `API Error: ${res.status}`); } return res.json(); } type CrudAction = "create" | "read" | "update" | "delete"; const logCrudAction = (action: CrudAction, resource: string, details?: unknown) => { const detailPayload = details ? { details } : undefined; console.log(`[CRUD:${action.toUpperCase()}] ${resource}`, detailPayload ?? ""); }; // Users API export const usersApi = { list: (params?: { page?: number; limit?: number }) => { logCrudAction("read", "users", params); const query = new URLSearchParams(); if (params?.page) query.set("page", String(params.page)); if (params?.limit) query.set("limit", String(params.limit)); const queryStr = query.toString(); return apiRequest>(`/api/v1/users${queryStr ? `?${queryStr}` : ""}`); }, create: (data: { name: string; email: string; password: string; role: string }) => { logCrudAction("create", "users", { name: data.name, email: data.email, role: data.role }); return apiRequest("/api/v1/users", { method: "POST", body: JSON.stringify(data), }); }, delete: (id: string) => { logCrudAction("delete", "users", { id }); return apiRequest(`/api/v1/users/${id}`, { method: "DELETE", }); }, }; // Companies API export const companiesApi = { list: () => { logCrudAction("read", "companies"); return apiRequest("/api/v1/companies"); }, create: (data: { name: string; slug: string; email?: string }) => { logCrudAction("create", "companies", data); return apiRequest("/api/v1/companies", { method: "POST", body: JSON.stringify(data), }); }, }; // Jobs API (public) export interface ApiJob { id: number; companyId: number; createdBy: number; title: string; description: string; salaryMin?: number; salaryMax?: number; salaryType?: string; employmentType?: string; workMode?: string; workingHours?: string; location?: string; regionId?: number; cityId?: number; requirements?: Record | string[]; benefits?: Record | string[]; visaSupport: boolean; languageLevel?: string; status: string; createdAt: string; updatedAt: string; companyName?: string; companyLogoUrl?: string; regionName?: string; cityName?: string; } export interface PaginatedResponse { data: T[]; pagination: { page: number; limit: number; total: number; }; } export const jobsApi = { list: (params?: { page?: number; limit?: number; companyId?: number }) => { logCrudAction("read", "jobs", params); const query = new URLSearchParams(); if (params?.page) query.set('page', String(params.page)); if (params?.limit) query.set('limit', String(params.limit)); if (params?.companyId) query.set('companyId', String(params.companyId)); const queryStr = query.toString(); return apiRequest>(`/jobs${queryStr ? `?${queryStr}` : ''}`); }, getById: (id: number) => { logCrudAction("read", "jobs", { id }); return apiRequest(`/jobs/${id}`); }, }; // Admin Backoffice API export interface AdminRoleAccess { role: string; description: string; actions: string[]; } export interface AdminLoginAudit { id: number; userId: string; identifier: string; roles: string; ipAddress?: string; userAgent?: string; createdAt: string; } export interface AdminCompany extends ApiCompany {} export interface AdminJob extends ApiJob {} export interface AdminTag { id: number; name: string; category: "area" | "level" | "stack"; active: boolean; createdAt: string; updatedAt: string; } export interface AdminCandidateApplication { id: number; jobTitle: string; company: string; status: "pending" | "reviewed" | "shortlisted" | "rejected" | "hired"; appliedAt: string; } export interface AdminCandidate { id: number; name: string; email?: string; phone?: string; location?: string; title?: string; experience?: string; avatarUrl?: string; bio?: string; skills: string[]; applications: AdminCandidateApplication[]; createdAt: string; } export interface AdminCandidateStats { totalCandidates: number; newCandidates: number; activeApplications: number; hiringRate: number; } export interface AdminCandidateListResponse { stats: AdminCandidateStats; candidates: AdminCandidate[]; } export const adminAccessApi = { listRoles: () => { logCrudAction("read", "admin/access/roles"); return apiRequest("/api/v1/admin/access/roles"); }, }; export const adminAuditApi = { listLogins: (limit = 50) => { logCrudAction("read", "admin/audit/logins", { limit }); return apiRequest(`/api/v1/admin/audit/logins?limit=${limit}`); }, }; export const adminCandidatesApi = { list: () => { logCrudAction("read", "admin/candidates"); return apiRequest("/api/v1/admin/candidates"); }, }; export const adminCompaniesApi = { list: (verified?: boolean) => { logCrudAction("read", "admin/companies", typeof verified === "boolean" ? { verified } : undefined); const query = typeof verified === "boolean" ? `?verified=${verified}` : ""; return apiRequest(`/api/v1/admin/companies${query}`); }, updateStatus: (id: number, data: { active?: boolean; verified?: boolean }) => { logCrudAction("update", "admin/companies", { id, ...data }); return apiRequest(`/api/v1/admin/companies/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, }; export const adminJobsApi = { list: (params?: { page?: number; limit?: number; status?: string }) => { logCrudAction("read", "admin/jobs", params); const query = new URLSearchParams(); if (params?.page) query.set("page", String(params.page)); if (params?.limit) query.set("limit", String(params.limit)); if (params?.status) query.set("status", params.status); const queryStr = query.toString(); return apiRequest>(`/api/v1/admin/jobs${queryStr ? `?${queryStr}` : ""}`); }, updateStatus: (id: number, status: string) => { logCrudAction("update", "admin/jobs/status", { id, status }); return apiRequest(`/api/v1/admin/jobs/${id}/status`, { method: "PATCH", body: JSON.stringify({ status }), }); }, duplicate: (id: number) => { logCrudAction("create", "admin/jobs/duplicate", { id }); return apiRequest(`/api/v1/admin/jobs/${id}/duplicate`, { method: "POST", }); }, }; export const adminTagsApi = { list: (category?: "area" | "level" | "stack") => { logCrudAction("read", "admin/tags", category ? { category } : undefined); const query = category ? `?category=${category}` : ""; return apiRequest(`/api/v1/admin/tags${query}`); }, create: (data: { name: string; category: "area" | "level" | "stack" }) => { logCrudAction("create", "admin/tags", data); return apiRequest("/api/v1/admin/tags", { method: "POST", body: JSON.stringify(data), }); }, update: (id: number, data: { name?: string; active?: boolean }) => { logCrudAction("update", "admin/tags", { id, ...data }); return apiRequest(`/api/v1/admin/tags/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, }; // Transform API job to frontend Job format export function transformApiJobToFrontend(apiJob: ApiJob): import('./types').Job { // Format salary let salary: string | undefined; if (apiJob.salaryMin && apiJob.salaryMax) { salary = `R$ ${apiJob.salaryMin.toLocaleString('en-US')} - R$ ${apiJob.salaryMax.toLocaleString('en-US')}`; } else if (apiJob.salaryMin) { salary = `From R$ ${apiJob.salaryMin.toLocaleString('en-US')}`; } else if (apiJob.salaryMax) { salary = `Up to R$ ${apiJob.salaryMax.toLocaleString('en-US')}`; } // Determine type type JobType = 'full-time' | 'part-time' | 'contract' | 'remote'; let type: JobType = 'full-time'; if (apiJob.employmentType === 'full-time') type = 'full-time'; else if (apiJob.employmentType === 'part-time') type = 'part-time'; else if (apiJob.employmentType === 'contract') type = 'contract'; else if (apiJob.workMode === 'remote' || apiJob.location?.toLowerCase().includes('remote') || apiJob.location?.toLowerCase().includes('remoto')) { type = 'remote'; } // Extract requirements const requirements: string[] = []; if (apiJob.requirements) { if (Array.isArray(apiJob.requirements)) { requirements.push(...apiJob.requirements.map(String)); } else if (typeof apiJob.requirements === 'object') { Object.values(apiJob.requirements).forEach(v => requirements.push(String(v))); } } return { id: String(apiJob.id), title: apiJob.title, company: apiJob.companyName || 'Company', location: apiJob.location || apiJob.cityName || 'Location not provided', type, workMode: apiJob.workMode as any, salary, description: apiJob.description, requirements: requirements.length > 0 ? requirements : ['View details'], postedAt: apiJob.createdAt?.split('T')[0] || new Date().toISOString().split('T')[0], }; }