From 300c76a15603c8a5e946c71fff2262ee48fbbb16 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Tue, 23 Dec 2025 19:37:47 -0300 Subject: [PATCH] fix(frontend): Export missing APIs for dashboard and backoffice --- frontend/src/lib/api.ts | 263 +++++++++++++++++++++++++++++++++++----- 1 file changed, 232 insertions(+), 31 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6c1ee9a..12286fb 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,9 +1,7 @@ import { toast } from "sonner"; -// import { useAuthStore } from "./store/auth-store"; // This file might not exist or be named differently -// For now, rely on localStorage directly as used in other functions -// import { useAuthStore } from "./store/auth-store"; +import { Job } from "./types"; -//const API_BASE_URL = "http://localhost:8521"; // Backend URL // Use this for dev +// const API_BASE_URL = "http://localhost:8521"; // Backend URL // Use this for dev const API_BASE_URL = ""; // Use this for production/setup with proxy /** @@ -17,7 +15,7 @@ function logCrudAction(action: string, entity: string, details?: any) { * Generic API Request Wrapper */ async function apiRequest(endpoint: string, options: RequestInit = {}): Promise { - const token = localStorage.getItem("token"); // Or useAuthStore.getState().token + const token = localStorage.getItem("token"); const headers = { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), @@ -31,12 +29,9 @@ async function apiRequest(endpoint: string, options: RequestInit = {}): Promi if (!response.ok) { const errorData = await response.json().catch(() => ({})); - // Use toast for feedback - // toast.error(`Error: ${errorData.message || response.statusText}`); throw new Error(errorData.message || `Request failed with status ${response.status}`); } - // Handle 204 No Content if (response.status === 204) { return {} as T; } @@ -44,6 +39,103 @@ async function apiRequest(endpoint: string, options: RequestInit = {}): Promi return response.json(); } +// --- Types --- + +export interface ApiUser { + id: string; + name: string; + email: string; + role: string; + status: string; + created_at: string; + avatarUrl?: string; // Add this +} + +export interface ApiJob { + id: string; + title: string; + companyName?: string; + companyId: string; + location: string; + type: string; // "full-time", etc. + workMode: string; // "remote", etc. + salaryMin?: number; + salaryMax?: number; + salaryType?: string; + description: string; + requirements: string; // Backend likely returns string, frontend might expect array? Warning here. + status: string; + createdAt: string; + isFeatured: boolean; + applicationCount?: number; +} + +export interface AdminCompany { + id: string; + name: string; + email?: string; + slug: string; + active: boolean; + verified: boolean; + created_at: string; +} + +export interface AdminJob { + id: number; + title: string; + companyName: string; + status: string; + createdAt: string; +} + +export interface AdminRoleAccess { + role: string; + description: string; + actions: string[]; +} + +export interface AdminLoginAudit { + id: string; + identifier: string; + roles: string; + ipAddress: string; + createdAt: string; +} + +export interface AdminTag { + id: number; + name: string; + category: "area" | "level" | "stack"; + active: boolean; +} + +export interface AdminCandidate { + id: number; + name: string; + email: string; + phone: string; + location: string; + title?: string; + experience?: string; + avatarUrl?: string; + bio?: string; + skills: string[]; + applications: { + id: string; + jobTitle: string; + company: string; + status: string; + }[]; +} + +export interface AdminCandidateStats { + totalCandidates: number; + newCandidates: number; + activeApplications: number; + hiringRate: number; +} + + // --- Auth API --- export const authApi = { login: (data: any) => { @@ -65,49 +157,94 @@ export const authApi = { }, }; -// --- Users (Admin) --- -export const adminUsersApi = { - list: (page = 1, limit = 10) => { +// --- Users API (General/Admin) --- +// Unified to match usage in users/page.tsx +export const usersApi = { + list: (params: { page: number; limit: number }) => { + const query = new URLSearchParams({ + page: params.page.toString(), + limit: params.limit.toString(), + }); return apiRequest<{ - data: any[]; - total: number; - page: number; - limit: number; - totalPages: number; - }>(`/api/v1/users?page=${page}&limit=${limit}`); + data: ApiUser[]; + pagination: { total: number; page: number; limit: number }; + }>(`/api/v1/users?${query.toString()}`); }, create: (data: any) => { - logCrudAction("create", "admin/users", data); + logCrudAction("create", "users", data); return apiRequest("/api/v1/users", { method: "POST", body: JSON.stringify(data), }); }, update: (id: string, data: any) => { - logCrudAction("update", "admin/users", { id, ...data }); + logCrudAction("update", "users", { id, ...data }); return apiRequest(`/api/v1/users/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, - updateStatus: (id: string, active: boolean) => { - logCrudAction("updateStatus", "admin/users", { id, active }); - return apiRequest(`/api/v1/users/${id}`, { - method: "PATCH", - body: JSON.stringify({ active }), - }); - }, delete: (id: string) => { - logCrudAction("delete", "admin/users", { id }); + logCrudAction("delete", "users", { id }); return apiRequest(`/api/v1/users/${id}`, { method: "DELETE", }); }, }; +export const adminUsersApi = usersApi; // Alias for backward compatibility if needed + + +// --- Admin Backoffice API --- +export const adminAccessApi = { + listRoles: () => apiRequest("/api/v1/admin/access/roles"), +}; + +export const adminAuditApi = { + listLogins: (limit = 20) => apiRequest(`/api/v1/admin/audit/logins?limit=${limit}`), +}; + +export const adminJobsApi = { + list: (params: { status?: string; page?: number; limit?: number }) => { + const query = new URLSearchParams(); + if (params.status) query.append("status", params.status); + if (params.page) query.append("page", params.page.toString()); + if (params.limit) query.append("limit", params.limit.toString()); + + return apiRequest<{ data: AdminJob[]; pagination: any }>(`/api/v1/admin/jobs?${query.toString()}`); + }, + updateStatus: (id: number, status: string) => + apiRequest(`/api/v1/admin/jobs/${id}/status`, { + method: "PATCH", + body: JSON.stringify({ status }), + }), + duplicate: (id: number) => + apiRequest(`/api/v1/admin/jobs/${id}/duplicate`, { + method: "POST", + }), +}; + +export const adminTagsApi = { + list: (category?: string) => { + const query = category ? `?category=${category}` : ""; + return apiRequest(`/api/v1/admin/tags${query}`); + }, + create: (data: { name: string; category: string }) => + apiRequest("/api/v1/admin/tags", { + method: "POST", + body: JSON.stringify(data), + }), + update: (id: number, data: { name?: string; active?: boolean }) => + apiRequest(`/api/v1/admin/tags/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }), +}; + +export const adminCandidatesApi = { + list: () => apiRequest<{ candidates: AdminCandidate[]; stats: AdminCandidateStats }>("/api/v1/admin/candidates"), +}; // --- Companies (Admin) --- -// Note: Backend endpoint is /api/v1/companies for listing (likely public or admin) and creation -// Assuming specific admin endpoints might be added later, for now using existing export const adminCompaniesApi = { list: (verified?: boolean, page = 1, limit = 10) => { const query = new URLSearchParams({ @@ -116,7 +253,7 @@ export const adminCompaniesApi = { ...(verified !== undefined && { verified: verified.toString() }) }); return apiRequest<{ - data: any[]; + data: AdminCompany[]; pagination: { page: number; limit: number; @@ -133,7 +270,7 @@ export const adminCompaniesApi = { }, updateStatus: (id: number, data: { active?: boolean; verified?: boolean }) => { logCrudAction("update", "admin/companies", { id, ...data }); - return apiRequest(`/api/v1/companies/${id}/status`, { // Ensure route exists + return apiRequest(`/api/v1/admin/companies/${id}/status`, { method: "PATCH", body: JSON.stringify(data), }); @@ -146,6 +283,70 @@ export const adminCompaniesApi = { } }; +// --- Jobs API (Public/Candidate) --- +export const jobsApi = { + list: (params: { + page?: number; + limit?: number; + q?: string; + location?: string; + type?: string; + workMode?: string; + }) => { + const query = new URLSearchParams(); + if (params.page) query.append("page", params.page.toString()); + if (params.limit) query.append("limit", params.limit.toString()); + if (params.q) query.append("q", params.q); + if (params.location) query.append("location", params.location); + if (params.type) query.append("type", params.type); + if (params.workMode) query.append("workMode", params.workMode); + + return apiRequest<{ + data: ApiJob[]; + pagination: { total: number; page: number; limit: number }; + }>(`/api/v1/jobs?${query.toString()}`); + }, + getById: (id: string) => apiRequest(`/api/v1/jobs/${id}`), +}; + +export const applicationsApi = { + create: (data: any) => + apiRequest("/api/v1/applications", { + method: "POST", + body: JSON.stringify(data), + }), +}; + +// --- Helper Functions --- +export function transformApiJobToFrontend(apiJob: ApiJob): Job { + // Requirements might come as a string derived from DB + let reqs: string[] = []; + if (apiJob.requirements) { + // Assuming it might be a JSON string or just text + // Simple split by newline for now if it's a block of text + reqs = apiJob.requirements.split('\n').filter(Boolean); + } + + return { + id: apiJob.id, + title: apiJob.title, + company: apiJob.companyName || "Unknown Company", + location: apiJob.location, + type: (apiJob.type as any) || "full-time", + workMode: (apiJob.workMode as any) || "onsite", + salary: apiJob.salaryMin && apiJob.salaryMax + ? `$${apiJob.salaryMin} - $${apiJob.salaryMax}` + : apiJob.salaryMin + ? `$${apiJob.salaryMin}+` + : undefined, + description: apiJob.description, + requirements: reqs, + postedAt: apiJob.createdAt, + isFeatured: apiJob.isFeatured, + }; +} + + // --- Notifications --- export interface Notification { id: string;