gohorsejobs/frontend/src/lib/api.ts
Tiago Yamamoto 364826c5c8 fix(dashboard): align CRUD pages with backend fields
jobs/page.tsx:
- Edit dialog now exposes all UpdateJobRequest fields: employmentType,
  workMode, salaryMin/max/type/currency, salaryNegotiable, languageLevel,
  visaSupport, location, status, isFeatured, description
- Fix AdminJob type to include all JobWithCompany fields returned by API
- Fix jobRows mapping that was hardcoding location/type/workMode/isFeatured
- Add isFeatured to CreateJobPayload type

applications/page.tsx:
- Fix status mismatch: reviewing→reviewed, interview→shortlisted, accepted→hired
- Align statusConfig labels/keys with backend constraint (pending/reviewed/
  shortlisted/rejected/hired)
- Update stats counters to use corrected status keys

companies/page.tsx:
- Add logoUrl and yearsInMarket to create and edit forms
- Populate editFormData from company object on edit open
- Send logoUrl/yearsInMarket in update payload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 18:27:30 -06:00

1077 lines
32 KiB
TypeScript

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<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers as Record<string, string>,
};
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<any>("/api/v1/auth/login", {
method: "POST",
body: JSON.stringify(data),
});
},
register: (data: any) => {
logCrudAction("register", "auth", { email: data.email });
return apiRequest<any>("/api/v1/auth/register", {
method: "POST",
body: JSON.stringify(data),
});
},
getCurrentUser: () => {
return apiRequest<ApiUser>("/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<any>("/api/v1/users", {
method: "POST",
body: JSON.stringify(data),
});
},
update: (id: string, data: any) => {
logCrudAction("update", "users", { id, ...data });
return apiRequest<any>(`/api/v1/users/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
},
delete: (id: string) => {
logCrudAction("delete", "users", { id });
return apiRequest<void>(`/api/v1/users/${id}`, {
method: "DELETE",
});
},
getMe: () => apiRequest<ApiUser & { bio?: string; skills?: string[]; experience?: any[]; education?: any[]; profilePictureUrl?: string; }>("/api/v1/users/me"),
updateMe: (data: any) =>
apiRequest<ApiUser>("/api/v1/users/me", {
method: "PUT",
body: JSON.stringify(data),
}),
};
export const adminUsersApi = usersApi;
// --- Admin Backoffice API ---
export const adminAccessApi = {
listRoles: () => apiRequest<AdminRoleAccess[]>("/api/v1/users/roles"),
};
export const adminAuditApi = {
listLogins: (limit = 20) => apiRequest<AdminLoginAudit[]>(`/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<void>(`/api/v1/jobs/${id}/status`, {
method: "PATCH",
body: JSON.stringify({ status }),
}),
duplicate: (id: string) =>
apiRequest<void>(`/api/v1/jobs/${id}/duplicate`, {
method: "POST",
}),
};
export const adminTagsApi = {
list: (category?: string) => {
const query = category ? `?category=${category}` : "";
return apiRequest<AdminTag[]>(`/api/v1/tags${query}`);
},
create: (data: { name: string; category: string }) =>
apiRequest<AdminTag>("/api/v1/tags", {
method: "POST",
body: JSON.stringify(data),
}),
update: (id: number, data: { name?: string; active?: boolean }) =>
apiRequest<AdminTag>(`/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<any>("/api/v1/companies", {
method: "POST",
body: JSON.stringify(data),
});
},
update: (id: string, data: Partial<AdminCompany>) => {
logCrudAction("update", "admin/companies", { id, ...data });
return apiRequest<AdminCompany>(`/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<void>(`/api/v1/companies/${id}/status`, {
method: "PATCH",
body: JSON.stringify(data),
});
},
delete: (id: string) => {
logCrudAction("delete", "admin/companies", { id });
return apiRequest<void>(`/api/v1/companies/${id}`, {
method: "DELETE"
});
}
};
// Companies API (Public/Shared)
export const companiesApi = {
list: () => apiRequest<AdminCompany[]>("/api/v1/companies"),
getById: (id: string) => apiRequest<ApiCompany>(`/api/v1/companies/${id}`),
create: (data: { name: string; slug: string; email?: string }) =>
apiRequest<AdminCompany>("/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<ApiJob>(`/api/v1/jobs/${id}`),
create: (data: CreateJobPayload) => {
logCrudAction("create", "jobs", data);
return apiRequest<ApiJob>("/api/v1/jobs", {
method: "POST",
body: JSON.stringify(data),
});
},
update: (id: string, data: Partial<CreateJobPayload>) => {
logCrudAction("update", "jobs", { id, ...data });
return apiRequest<ApiJob>(`/api/v1/jobs/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
},
delete: (id: string) => {
logCrudAction("delete", "jobs", { id });
return apiRequest<void>(`/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<Array<{
id: string;
searchQuery?: string;
location?: string;
isActive: boolean;
frequency: string;
}>>("/api/v1/alerts/me"),
deleteAlert: (id: string) => apiRequest<void>(`/api/v1/alerts/${id}`, {
method: "DELETE",
}),
toggleAlert: (id: string, active: boolean) => apiRequest<void>(`/api/v1/alerts/${id}/toggle`, {
method: "PATCH",
body: JSON.stringify({ active }),
}),
// Favorites - wrap with silent error handling
getFavorites: () => apiRequest<Array<{
id: string;
jobId: string;
jobTitle: string;
companyName: string;
location?: string;
}>>("/api/v1/favorites"),
addFavorite: (jobId: string) => apiRequest<{ id: string }>(`/api/v1/favorites/${jobId}`, {
method: "POST",
}),
removeFavorite: (jobId: string) => apiRequest<void>(`/api/v1/favorites/${jobId}`, {
method: "DELETE",
}),
checkFavorite: (jobId: string) => apiRequest<{ isFavorite: boolean }>(`/api/v1/favorites/${jobId}/check`),
// Companies
getFollowing: () => apiRequest<Array<{
companyId: string;
companyName: string;
jobsCount: number;
}>>("/api/v1/companies/following"),
followCompany: (companyId: string) => apiRequest<{ id: string }>(`/api/v1/companies/follow/${companyId}`, {
method: "POST",
}),
unfollowCompany: (companyId: string) => apiRequest<void>(`/api/v1/companies/follow/${companyId}`, {
method: "DELETE",
}),
checkFollowing: (companyId: string) => apiRequest<{ isFollowing: boolean }>(`/api/v1/companies/followed/check/${companyId}`),
getCompaniesWithJobs: () => apiRequest<Array<{
companyId: string;
companyName: string;
companyLogoUrl?: string;
jobsCount: number;
}>>("/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<ApiApplication>("/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<any[]>(`/api/v1/applications?${query.toString()}`);
},
listMyApplications: () => {
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
},
delete: (id: string) => {
return apiRequest<void>(`/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<Notification[]>("/api/v1/notifications");
},
markAsRead: (id: string) => {
return apiRequest<void>(`/api/v1/notifications/${id}/read`, {
method: "PATCH",
});
},
markAllAsRead: () => {
return apiRequest<void>("/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<Ticket>("/api/v1/support/tickets", {
method: "POST",
body: JSON.stringify(data),
});
},
list: () => {
return apiRequest<Ticket[]>("/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<TicketMessage>(`/api/v1/support/tickets/${id}/messages`, {
method: "POST",
body: JSON.stringify({ message }),
});
},
listAll: () => {
return apiRequest<Ticket[]>("/api/v1/support/tickets/all");
},
update: (id: string, data: { status?: string; priority?: string }) => {
return apiRequest<Ticket>(`/api/v1/support/tickets/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
},
delete: (id: string) => {
return apiRequest<void>(`/api/v1/support/tickets/${id}`, {
method: "DELETE",
});
},
};
// --- Profile ---
export const profileApi = {
update: (data: { name?: string; email?: string; phone?: string; bio?: string }) => {
return apiRequest<any>("/api/v1/users/me/profile", {
method: "PATCH",
body: JSON.stringify(data),
});
},
updatePassword: (data: { currentPassword: string; newPassword: string }) => {
return apiRequest<void>("/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<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers as Record<string, string>,
};
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<Plan[]>("/plans"),
getById: (id: string) => backofficeRequest<Plan>(`/plans/${id}`),
create: (data: Omit<Plan, "id">) => backofficeRequest<Plan>("/plans", {
method: "POST",
body: JSON.stringify(data),
}),
update: (id: string, data: Partial<Plan>) => {
const { id: _, ...updateData } = data;
return backofficeRequest<Plan>(`/plans/${id}`, {
method: "PATCH",
body: JSON.stringify(updateData),
});
},
delete: (id: string) => backofficeRequest<void>(`/plans/${id}`, {
method: "DELETE",
}),
};
export const backofficeApi = {
admin: {
getStats: () => backofficeRequest<DashboardStats>("/admin/stats"),
getRevenue: () => backofficeRequest<RevenueByMonth[]>("/admin/revenue"),
getSubscriptionsByPlan: () => backofficeRequest<SubscriptionsByPlan[]>("/admin/subscriptions-by-plan"),
},
stripe: {
createCheckoutSession: (data: CheckoutSessionRequest) =>
backofficeRequest<CheckoutSessionResponse>("/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<any>(`/stripe/subscriptions/${customerId}`),
},
externalServices: {
purgeCloudflareCache: () => backofficeRequest<void>("/external-services/cloudflare/purge", { method: "POST" })
}
};
export const fcmApi = {
saveToken: (token: string, platform: 'web' | 'android' | 'ios' = 'web') => {
return apiRequest<void>("/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<Conversation[]>("/api/v1/conversations"),
listMessages: (conversationId: string) => apiRequest<Message[]>(`/api/v1/conversations/${conversationId}/messages`),
sendMessage: (conversationId: string, content: string) => apiRequest<Message>(`/api/v1/conversations/${conversationId}/messages`, {
method: "POST",
body: JSON.stringify({ content }),
}),
};
export const settingsApi = {
get: async (key: string): Promise<any> => {
const res = await apiRequest<any>(`/api/v1/system/settings/${key}`)
return res
},
save: async (key: string, value: any): Promise<void> => {
await apiRequest<void>(`/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<void>("/api/v1/system/credentials", {
method: "POST",
body: JSON.stringify({ serviceName, payload }),
}),
delete: (serviceName: string) => apiRequest<void>(`/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<EmailTemplate[]>("/api/v1/admin/email-templates"),
get: (slug: string) => apiRequest<EmailTemplate>(`/api/v1/admin/email-templates/${slug}`),
create: (data: Partial<EmailTemplate>) => apiRequest<EmailTemplate>("/api/v1/admin/email-templates", {
method: "POST",
body: JSON.stringify(data),
}),
update: (slug: string, data: Partial<EmailTemplate>) => apiRequest<EmailTemplate>(`/api/v1/admin/email-templates/${slug}`, {
method: "PUT",
body: JSON.stringify(data),
}),
delete: (slug: string) => apiRequest<void>(`/api/v1/admin/email-templates/${slug}`, {
method: "DELETE",
}),
};
export const emailSettingsApi = {
get: () => apiRequest<EmailSettings>("/api/v1/admin/email-settings"),
update: (data: Partial<EmailSettings>) => apiRequest<EmailSettings>("/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<Country[]>("/api/v1/locations/countries"),
listStates: (countryId: number | string) => apiRequest<State[]>(`/api/v1/locations/countries/${countryId}/states`),
listCities: (stateId: number | string) => apiRequest<City[]>(`/api/v1/locations/states/${stateId}/cities`),
search: async (query: string, countryId: number | string) => {
const res = await apiRequest<LocationSearchResult[]>(`/api/v1/locations/search?q=${encodeURIComponent(query)}&country_id=${countryId}`);
return res || [];
},
};