import { toast } from "sonner"; import { Job } from "./types"; import { getApiUrl, getBackofficeUrl, initConfig } from "./config"; // Track if we've shown an auth error toast to avoid spam let hasShownAuthError = false; /** * Helper to log CRUD actions for the 'Activity Log' or console */ function logCrudAction(action: string, entity: string, details?: any) { // Only log in development if (process.env.NODE_ENV === 'development') { console.log(`[CRUD] ${action.toUpperCase()} ${entity}`, details); } } /** * Generic API Request Wrapper */ async function apiRequest(endpoint: string, options: RequestInit = {}): Promise { const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null; const headers: Record = { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.headers as Record, }; if (options.body) { headers["Content-Type"] = "application/json"; } const response = await fetch(`${getApiUrl()}${endpoint}`, { ...options, headers, credentials: "include", }); if (!response.ok) { // Handle 401 silently - it's expected for unauthenticated users if (response.status === 401) { // Clear any stale auth data if (typeof window !== 'undefined') { localStorage.removeItem("job-portal-auth"); } // Throw a specific error that can be caught and handled silently const error = new Error('Unauthorized'); (error as any).status = 401; (error as any).silent = true; throw error; } const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `Request failed with status ${response.status}`); } if (response.status === 204) { return {} as T; } return response.json(); } // --- Types --- export interface ApiUser { id: string; name: string; fullName?: string; email: string; role: "superadmin" | "admin" | "recruiter" | "candidate" | string; status: string; created_at: string; avatarUrl?: string; phone?: string; bio?: string; companyId?: string; } export interface ApiJob { id: string; title: string; companyName?: string; companyLogoUrl?: string; companyId: string; location?: string | null; type?: string; // Legacy alias employmentType?: string; workMode?: string | null; // "remote", etc. salaryMin?: number; salaryMax?: number; salaryType?: string; currency?: string; description: string; requirements?: unknown; status: string; createdAt: string; datePosted?: string; isFeatured: boolean; applicationCount?: number; } export interface ApiCompany { id: string; name: string; description?: string | null; website?: string | null; logoUrl?: string | null; employeeCount?: string | null; foundedYear?: number | null; active: boolean; verified: boolean; } export interface AdminCompany { id: string; name: string; email?: string; slug: string; type?: string; document?: string; address?: string; regionId?: number; cityId?: number; phone?: string; website?: string; logoUrl?: string; description?: string; active: boolean; verified: boolean; createdAt: string; updatedAt?: string; } export interface AdminJob { id: string; companyId: string; title: string; description?: string; companyName: string; location?: string; employmentType?: string; workMode?: string; workingHours?: string; salaryMin?: number; salaryMax?: number; salaryType?: string; currency?: string; salaryNegotiable?: boolean; languageLevel?: string; visaSupport?: boolean; status: string; isFeatured?: boolean; applicationsCount?: number; 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: string; 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) => { logCrudAction("login", "auth", { email: data.email }); return apiRequest("/api/v1/auth/login", { method: "POST", body: JSON.stringify(data), }); }, register: (data: any) => { logCrudAction("register", "auth", { email: data.email }); return apiRequest("/api/v1/auth/register", { method: "POST", body: JSON.stringify(data), }); }, getCurrentUser: () => { return apiRequest("/api/v1/users/me"); }, }; // --- Users API (General/Admin) --- export const usersApi = { list: (params: { page: number; limit: number }) => { const query = new URLSearchParams({ page: params.page.toString(), limit: params.limit.toString(), }); return apiRequest<{ data: ApiUser[]; pagination: { total: number; page: number; limit: number }; }>(`/api/v1/users?${query.toString()}`); }, create: (data: any) => { logCrudAction("create", "users", data); return apiRequest("/api/v1/users", { method: "POST", body: JSON.stringify(data), }); }, update: (id: string, data: any) => { logCrudAction("update", "users", { id, ...data }); return apiRequest(`/api/v1/users/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, delete: (id: string) => { logCrudAction("delete", "users", { id }); return apiRequest(`/api/v1/users/${id}`, { method: "DELETE", }); }, getMe: () => apiRequest("/api/v1/users/me"), updateMe: (data: any) => apiRequest("/api/v1/users/me", { method: "PUT", body: JSON.stringify(data), }), }; export const adminUsersApi = usersApi; // --- Admin Backoffice API --- export const adminAccessApi = { listRoles: () => apiRequest("/api/v1/users/roles"), }; export const adminAuditApi = { listLogins: (limit = 20) => apiRequest(`/api/v1/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/jobs/moderation?${query.toString()}`); }, updateStatus: (id: string, status: string) => apiRequest(`/api/v1/jobs/${id}/status`, { method: "PATCH", body: JSON.stringify({ status }), }), duplicate: (id: string) => apiRequest(`/api/v1/jobs/${id}/duplicate`, { method: "POST", }), }; export const adminTagsApi = { list: (category?: string) => { const query = category ? `?category=${category}` : ""; return apiRequest(`/api/v1/tags${query}`); }, create: (data: { name: string; category: string }) => apiRequest("/api/v1/tags", { method: "POST", body: JSON.stringify(data), }), update: (id: number, data: { name?: string; active?: boolean }) => apiRequest(`/api/v1/tags/${id}`, { method: "PATCH", body: JSON.stringify(data), }), }; export const adminCandidatesApi = { list: () => apiRequest<{ candidates: AdminCandidate[]; stats: AdminCandidateStats }>("/api/v1/candidates"), }; // --- Companies (Admin) --- export const adminCompaniesApi = { list: (verified?: boolean, page = 1, limit = 10) => { const query = new URLSearchParams({ page: page.toString(), limit: limit.toString(), ...(verified !== undefined && { verified: verified.toString() }) }); return apiRequest<{ data: AdminCompany[]; pagination: { page: number; limit: number; total: number; } }>(`/api/v1/companies?${query.toString()}`); }, create: (data: any) => { logCrudAction("create", "admin/companies", data); return apiRequest("/api/v1/companies", { method: "POST", body: JSON.stringify(data), }); }, update: (id: string, data: Partial) => { logCrudAction("update", "admin/companies", { id, ...data }); return apiRequest(`/api/v1/companies/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, updateStatus: (id: string, data: { active?: boolean; verified?: boolean }) => { logCrudAction("update", "admin/companies", { id, ...data }); return apiRequest(`/api/v1/companies/${id}/status`, { method: "PATCH", body: JSON.stringify(data), }); }, delete: (id: string) => { logCrudAction("delete", "admin/companies", { id }); return apiRequest(`/api/v1/companies/${id}`, { method: "DELETE" }); } }; // Companies API (Public/Shared) export const companiesApi = { list: () => apiRequest("/api/v1/companies"), getById: (id: string) => apiRequest(`/api/v1/companies/${id}`), create: (data: { name: string; slug: string; email?: string }) => apiRequest("/api/v1/companies", { method: "POST", body: JSON.stringify(data), }), }; // --- Jobs API (Public/Candidate) --- export interface CreateJobPayload { companyId: string; title: string; description: string; location?: string; cityId?: number; regionId?: number; employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent'; workMode?: 'onsite' | 'hybrid' | 'remote'; workingHours?: string; salaryMin?: number; salaryMax?: number; salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly'; currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY' | 'CNY' | 'AED' | 'CAD' | 'AUD' | 'CHF'; salaryNegotiable?: boolean; languageLevel?: string; visaSupport?: boolean; requirements?: { resumeRequirement?: string; applicationChannel?: string; applicationEmail?: string | null; applicationUrl?: string | null; applicationPhone?: string | null; }; status: 'draft' | 'review' | 'open' | 'paused' | 'closed' | 'published'; isFeatured?: boolean; } export const jobsApi = { list: (params: { page?: number; limit?: number; q?: string; location?: string; type?: string; workMode?: string; companyId?: string; salaryMin?: number; salaryMax?: number; currency?: string; visaSupport?: boolean; sortBy?: string; datePosted?: 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("employmentType", params.type); if (params.workMode) query.append("workMode", params.workMode); if (params.companyId) query.append("companyId", params.companyId); if (params.salaryMin) query.append("salaryMin", params.salaryMin.toString()); if (params.salaryMax) query.append("salaryMax", params.salaryMax.toString()); if (params.currency) query.append("currency", params.currency); if (params.visaSupport) query.append("visaSupport", "true"); if (params.sortBy) query.append("sortBy", params.sortBy); if (params.datePosted) query.append("datePosted", params.datePosted); return apiRequest<{ data: ApiJob[]; pagination: { total: number; page: number; limit: number }; }>(`/api/v1/jobs?${query.toString()}`); }, getById: (id: string) => apiRequest(`/api/v1/jobs/${id}`), create: (data: CreateJobPayload) => { logCrudAction("create", "jobs", data); return apiRequest("/api/v1/jobs", { method: "POST", body: JSON.stringify(data), }); }, update: (id: string, data: Partial) => { logCrudAction("update", "jobs", { id, ...data }); return apiRequest(`/api/v1/jobs/${id}`, { method: "PUT", body: JSON.stringify(data), }); }, delete: (id: string) => { logCrudAction("delete", "jobs", { id }); return apiRequest(`/api/v1/jobs/${id}`, { method: "DELETE", }); }, // Job Alerts createAlert: (data: { searchQuery?: string; location?: string; employmentType?: string; workMode?: string; salaryMin?: number; salaryMax?: number; currency?: string; frequency?: string; email?: string; }) => apiRequest<{ id: string }>("/api/v1/alerts", { method: "POST", body: JSON.stringify(data), }), getMyAlerts: () => apiRequest>("/api/v1/alerts/me"), deleteAlert: (id: string) => apiRequest(`/api/v1/alerts/${id}`, { method: "DELETE", }), toggleAlert: (id: string, active: boolean) => apiRequest(`/api/v1/alerts/${id}/toggle`, { method: "PATCH", body: JSON.stringify({ active }), }), // Favorites - wrap with silent error handling getFavorites: () => apiRequest>("/api/v1/favorites"), addFavorite: (jobId: string) => apiRequest<{ id: string }>(`/api/v1/favorites/${jobId}`, { method: "POST", }), removeFavorite: (jobId: string) => apiRequest(`/api/v1/favorites/${jobId}`, { method: "DELETE", }), checkFavorite: (jobId: string) => apiRequest<{ isFavorite: boolean }>(`/api/v1/favorites/${jobId}/check`), // Companies getFollowing: () => apiRequest>("/api/v1/companies/following"), followCompany: (companyId: string) => apiRequest<{ id: string }>(`/api/v1/companies/follow/${companyId}`, { method: "POST", }), unfollowCompany: (companyId: string) => apiRequest(`/api/v1/companies/follow/${companyId}`, { method: "DELETE", }), checkFollowing: (companyId: string) => apiRequest<{ isFollowing: boolean }>(`/api/v1/companies/followed/check/${companyId}`), getCompaniesWithJobs: () => apiRequest>("/api/v1/companies/with-jobs"), }; // Applications API export interface ApiApplication { id: number; jobId: number; userId?: number; name?: string; email?: string; phone?: string; resumeUrl?: string; status: string; createdAt: string; } export const applicationsApi = { create: (data: any) => apiRequest("/api/v1/applications", { method: "POST", body: JSON.stringify(data), }), list: (params: { jobId?: string; companyId?: string }) => { const query = new URLSearchParams(); if (params.jobId) query.append("jobId", params.jobId); if (params.companyId) query.append("companyId", params.companyId); return apiRequest(`/api/v1/applications?${query.toString()}`); }, listMyApplications: () => { return apiRequest("/api/v1/applications/me"); }, delete: (id: string) => { return apiRequest(`/api/v1/applications/${id}`, { method: "DELETE" }); }, }; // Storage API export const storageApi = { getUploadUrl: (filename: string, contentType: string) => apiRequest<{ uploadUrl: string; key: string; publicUrl: string }>( "/api/v1/storage/upload-url", { method: "POST", body: JSON.stringify({ filename, contentType }) } ), uploadFile: async (file: File, folder = "uploads") => { const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null; const formData = new FormData(); formData.append('file', file); formData.append('folder', folder); const response = await fetch(`${getApiUrl()}/api/v1/storage/upload`, { method: 'POST', body: formData, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, credentials: 'include', }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to upload file to storage: ${errorText}`); } const data = await response.json(); return { key: data.key, publicUrl: data.publicUrl || data.url }; }, testConnection: () => apiRequest<{ message: string }>("/api/v1/admin/storage/test-connection", { method: "POST" }), }; // --- Helper Functions --- export function transformApiJobToFrontend(apiJob: ApiJob): Job { let reqs: string[] = []; if (apiJob.requirements) { if (Array.isArray(apiJob.requirements)) { reqs = apiJob.requirements.map(String).filter(Boolean); } else if (typeof apiJob.requirements === 'string') { if (apiJob.requirements.startsWith('[')) { try { reqs = JSON.parse(apiJob.requirements); } catch (e) { reqs = apiJob.requirements.split('\n').filter(Boolean); } } else { reqs = apiJob.requirements.split('\n').filter(Boolean); } } } let salaryLabel: string | undefined; if (apiJob.salaryMin && apiJob.salaryMax) { salaryLabel = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`; } else if (apiJob.salaryMin) { salaryLabel = `A partir de R$ ${apiJob.salaryMin.toLocaleString('pt-BR')}`; } else if (apiJob.salaryMax) { salaryLabel = `Até R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`; } 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: salaryLabel, description: apiJob.description, requirements: reqs, postedAt: apiJob.createdAt, isFeatured: apiJob.isFeatured, }; } // --- Notifications --- export interface Notification { id: string; userId: number; type: 'info' | 'success' | 'warning' | 'error'; title: string; message: string; link?: string; read: boolean; createdAt: string; } export const notificationsApi = { list: () => { return apiRequest("/api/v1/notifications"); }, markAsRead: (id: string) => { return apiRequest(`/api/v1/notifications/${id}/read`, { method: "PATCH", }); }, markAllAsRead: () => { return apiRequest("/api/v1/notifications/read-all", { method: "PATCH", }); }, }; // --- Support Tickets --- export interface Ticket { id: string; userId: number; subject: string; status: 'open' | 'in_progress' | 'closed'; priority: 'low' | 'medium' | 'high'; createdAt: string; updatedAt: string; } export interface TicketMessage { id: string; ticketId: string; userId: number; message: string; createdAt: string; } export const ticketsApi = { create: (data: { subject: string; priority: string; message?: string }) => { return apiRequest("/api/v1/support/tickets", { method: "POST", body: JSON.stringify(data), }); }, list: () => { return apiRequest("/api/v1/support/tickets"); }, get: (id: string) => { return apiRequest<{ ticket: Ticket; messages: TicketMessage[] }>(`/api/v1/support/tickets/${id}`); }, sendMessage: (id: string, message: string) => { return apiRequest(`/api/v1/support/tickets/${id}/messages`, { method: "POST", body: JSON.stringify({ message }), }); }, listAll: () => { return apiRequest("/api/v1/support/tickets/all"); }, update: (id: string, data: { status?: string; priority?: string }) => { return apiRequest(`/api/v1/support/tickets/${id}`, { method: "PATCH", body: JSON.stringify(data), }); }, delete: (id: string) => { return apiRequest(`/api/v1/support/tickets/${id}`, { method: "DELETE", }); }, }; // --- Profile --- export const profileApi = { update: (data: { name?: string; email?: string; phone?: string; bio?: string }) => { return apiRequest("/api/v1/users/me/profile", { method: "PATCH", body: JSON.stringify(data), }); }, updatePassword: (data: { currentPassword: string; newPassword: string }) => { return apiRequest("/api/v1/users/me/password", { method: "PATCH", body: JSON.stringify(data), }); }, async uploadAvatar(file: File) { const { url, key, publicUrl } = await apiRequest<{ url: string; key: string; publicUrl?: string }>( `/api/v1/storage/upload-url?filename=${encodeURIComponent(file.name)}&contentType=${encodeURIComponent(file.type)}&folder=avatars` ); const uploadRes = await fetch(url, { method: "PUT", headers: { "Content-Type": file.type, }, body: file, }); if (!uploadRes.ok) { throw new Error("Failed to upload image to storage"); } const avatarUrl = publicUrl || key; const res = await fetch(`${getApiUrl()}/api/v1/users/me/profile`, { method: "PATCH", headers: { "Content-Type": "application/json", }, credentials: "include", body: JSON.stringify({ avatarUrl }) }); if (!res.ok) throw new Error("Failed to update profile avatar"); return res.json(); }, }; // ============================================================================= // BACKOFFICE API (Stripe, Admin Dashboard, etc.) // ============================================================================= async function backofficeRequest(endpoint: string, options: RequestInit = {}): Promise { const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null; const headers: Record = { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.headers as Record, }; if (options.body) { headers["Content-Type"] = "application/json"; } const response = await fetch(`${getBackofficeUrl()}${endpoint}`, { ...options, headers, credentials: "include", }); if (!response.ok) { if (response.status === 401) { const error = new Error('Unauthorized'); (error as any).status = 401; (error as any).silent = true; throw error; } const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `Request failed with status ${response.status}`); } if (response.status === 204) { return {} as T; } return response.json(); } export interface DashboardStats { totalCompanies: number; activeSubscriptions: number; monthlyRevenue: number; newCompaniesThisMonth: number; } export interface RevenueByMonth { month: string; revenue: number; } export interface SubscriptionsByPlan { plan: string; count: number; } export interface CheckoutSessionRequest { priceId: string; successUrl: string; cancelUrl: string; } export interface CheckoutSessionResponse { url: string; sessionId: string; } export interface Plan { id: string; name: string; description: string; monthlyPrice: number; yearlyPrice: number; features: string[]; popular?: boolean; } export const plansApi = { getAll: () => backofficeRequest("/plans"), getById: (id: string) => backofficeRequest(`/plans/${id}`), create: (data: Omit) => backofficeRequest("/plans", { method: "POST", body: JSON.stringify(data), }), update: (id: string, data: Partial) => { const { id: _, ...updateData } = data; return backofficeRequest(`/plans/${id}`, { method: "PATCH", body: JSON.stringify(updateData), }); }, delete: (id: string) => backofficeRequest(`/plans/${id}`, { method: "DELETE", }), }; export const backofficeApi = { admin: { getStats: () => backofficeRequest("/admin/stats"), getRevenue: () => backofficeRequest("/admin/revenue"), getSubscriptionsByPlan: () => backofficeRequest("/admin/subscriptions-by-plan"), }, stripe: { createCheckoutSession: (data: CheckoutSessionRequest) => backofficeRequest("/stripe/checkout", { method: "POST", body: JSON.stringify(data), }), createBillingPortal: (returnUrl: string) => backofficeRequest<{ url: string }>("/stripe/portal", { method: "POST", body: JSON.stringify({ returnUrl }), }), listSubscriptions: (customerId: string) => backofficeRequest(`/stripe/subscriptions/${customerId}`), }, externalServices: { purgeCloudflareCache: () => backofficeRequest("/external-services/cloudflare/purge", { method: "POST" }) } }; export const fcmApi = { saveToken: (token: string, platform: 'web' | 'android' | 'ios' = 'web') => { return apiRequest("/api/v1/tokens", { method: "POST", body: JSON.stringify({ token, platform }), }); }, }; // --- Chat --- export interface Message { id: string; conversationId: string; senderId: string; content: string; createdAt: string; isMine: boolean; } export interface Conversation { id: string; lastMessage: string; lastMessageAt: string; participantName: string; participantAvatar?: string; unreadCount?: number; } export const chatApi = { listConversations: () => apiRequest("/api/v1/conversations"), listMessages: (conversationId: string) => apiRequest(`/api/v1/conversations/${conversationId}/messages`), sendMessage: (conversationId: string, content: string) => apiRequest(`/api/v1/conversations/${conversationId}/messages`, { method: "POST", body: JSON.stringify({ content }), }), }; export const settingsApi = { get: async (key: string): Promise => { const res = await apiRequest(`/api/v1/system/settings/${key}`) return res }, save: async (key: string, value: any): Promise => { await apiRequest(`/api/v1/system/settings/${key}`, { method: "POST", body: JSON.stringify(value), }) } } // --- System Credentials --- export interface ConfiguredService { service_name: string; updated_at: string; updated_by: string; is_configured: boolean; } export const credentialsApi = { list: () => apiRequest<{ services: ConfiguredService[] }>("/api/v1/system/credentials"), save: (serviceName: string, payload: any) => apiRequest("/api/v1/system/credentials", { method: "POST", body: JSON.stringify({ serviceName, payload }), }), delete: (serviceName: string) => apiRequest(`/api/v1/system/credentials/${serviceName}`, { method: "DELETE", }), }; // --- Email Templates & Settings --- export interface EmailTemplate { id: string; slug: string; subject: string; body_html: string; variables: string[]; created_at: string; updated_at: string; } export interface EmailSettings { id?: string; provider: string; smtp_host?: string; smtp_port?: number; smtp_user?: string; smtp_pass?: string; smtp_secure: boolean; sender_name: string; sender_email: string; amqp_url?: string; is_active?: boolean; updated_at?: string; } export const emailTemplatesApi = { list: () => apiRequest("/api/v1/admin/email-templates"), get: (slug: string) => apiRequest(`/api/v1/admin/email-templates/${slug}`), create: (data: Partial) => apiRequest("/api/v1/admin/email-templates", { method: "POST", body: JSON.stringify(data), }), update: (slug: string, data: Partial) => apiRequest(`/api/v1/admin/email-templates/${slug}`, { method: "PUT", body: JSON.stringify(data), }), delete: (slug: string) => apiRequest(`/api/v1/admin/email-templates/${slug}`, { method: "DELETE", }), }; export const emailSettingsApi = { get: () => apiRequest("/api/v1/admin/email-settings"), update: (data: Partial) => apiRequest("/api/v1/admin/email-settings", { method: "PUT", body: JSON.stringify(data), }), }; // --- Location API --- export interface Country { id: number; name: string; iso2: string; iso3: string; phone_code: string; currency: string; emoji: string; emojiU: string; } export interface State { id: number; name: string; country_id: number; country_code: string; iso2: string; type: string; latitude: number; longitude: number; } export interface City { id: number; name: string; state_id: number; country_id: number; latitude: number; longitude: number; } export interface LocationSearchResult { id: number; name: string; type: 'state' | 'city'; country_id: number; state_id?: number; region_name?: string; } export const locationsApi = { listCountries: () => apiRequest("/api/v1/locations/countries"), listStates: (countryId: number | string) => apiRequest(`/api/v1/locations/countries/${countryId}/states`), listCities: (stateId: number | string) => apiRequest(`/api/v1/locations/states/${stateId}/cities`), search: async (query: string, countryId: number | string) => { const res = await apiRequest(`/api/v1/locations/search?q=${encodeURIComponent(query)}&country_id=${countryId}`); return res || []; }, };