/opt/canhelp/apps/web/src/lib
Edit: /opt/canhelp/apps/web/src/lib/api.ts (44017B)
import type { Task, Offer, ChatRoom, Message, Notification, Review, Category, Paginated } from '@canhelp/shared'
// SSR: use INTERNAL_API_URL (http://localhost:4000) to avoid external DNS/SSL round-trip
// Browser: use NEXT_PUBLIC_API_URL (https://api.canhelp.gr)
const BASE =
typeof window === 'undefined'
? (process.env.INTERNAL_API_URL || 'http://localhost:4000')
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000')
export async function request
(path: string, init?: RequestInit): Promise {
const url = `${BASE}/api${path}`
if (typeof window === 'undefined') console.log('[api] SSR fetch', url)
const res = await fetch(url, {
...init,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
const msg = typeof err.error === 'string'
? err.error
: typeof err.message === 'string'
? err.message
: res.statusText || 'Request failed'
throw new Error(msg)
}
return res.json()
}
// ─── Tasks ────────────────────────────────────────────────────────────────
export const getTasks = (params?: Record) => {
const qs = params ? '?' + new URLSearchParams(params) : ''
return request>(`/tasks${qs}`)
}
export const getTaskBudgetRange = (params?: Record) => {
const qs = params ? '?' + new URLSearchParams(params) : ''
return request<{ min: number; max: number }>(`/tasks/budget-range${qs}`)
}
export const searchTasks = (params: Record) =>
request>(`/tasks/search?${new URLSearchParams(params)}`)
export const getMyTasks = () => request('/tasks/my')
export const getTasksForSpecialist = (params?: Record) => {
const qs = params ? '?' + new URLSearchParams(params) : ''
return request<{ data: any[]; total: number; page: number; limit: number; noCards?: boolean }>(`/tasks/for-me${qs}`)
}
export const getTaskCountsByCategory = () =>
request>('/tasks/counts-by-category')
export const getTask = (id: string) => request<{ task: Task; customer: any }>(`/tasks/${id}`)
export const createTask = (data: Partial) =>
request('/tasks', { method: 'POST', body: JSON.stringify(data) })
export const updateTask = (id: string, data: Partial) =>
request(`/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const completeTask = (id: string) =>
request(`/tasks/${id}/complete`, { method: 'PATCH' })
export const cancelTask = (id: string) =>
request(`/tasks/${id}/cancel`, { method: 'PATCH' })
// ─── Offers ────────────────────────────────────────────────────────────────
export const getOffers = (taskId: string) =>
request>(`/offers?taskId=${taskId}`)
export const getMyOffers = () =>
request>('/offers/my')
export const createOffer = (data: { taskId: string; price: number; message?: string }) =>
request('/offers', { method: 'POST', body: JSON.stringify(data) })
export const acceptOffer = (id: string) =>
request(`/offers/${id}/accept`, { method: 'PATCH' })
export const declineOffer = (id: string) =>
request(`/offers/${id}/decline`, { method: 'PATCH' })
export const getUserProfile = (id: string, locale?: string) =>
request(`/users/${id}/profile${locale ? `?locale=${encodeURIComponent(locale)}` : ''}`)
export const getSpecialists = (params?: Record) => {
const qs = params ? '?' + new URLSearchParams(params) : ''
return request<{ data: any[]; total: number; page: number; limit: number }>(`/users/specialists${qs}`)
}
export const createReview = (data: { taskId: string; targetId: string; rating: number; comment?: string }) =>
request('/reviews', { method: 'POST', body: JSON.stringify(data) })
export const getTaskReviews = (taskId: string) =>
request(`/reviews/task/${taskId}`)
export const getMyReviews = () =>
request>('/reviews/my')
// ─── Chat ──────────────────────────────────────────────────────────────────
export const getChatRooms = () =>
request | null; imageUrl?: string | null } | null }>>('/chat/rooms')
export const getOrCreateDirectRoom = (userId: string) =>
request<{ room: ChatRoom; created: boolean }>(`/chat/direct/${userId}`)
export const getCanMessage = (userId: string) =>
request<{ allowed: boolean; reason?: string; code?: string; senderTier?: string; recipientTier?: string }>(`/chat/can-message/${userId}`)
export const getMessages = (roomId: string) =>
request | null }; sender: { id: string; name: string; firstName?: string | null; lastName?: string | null; image?: string } | null }>>(`/chat/rooms/${roomId}/messages`)
export const sendMessage = (roomId: string, body: string) =>
request('/chat/messages', { method: 'POST', body: JSON.stringify({ roomId, body }) })
// ─── Notifications ────────────────────────────────────────────────────────
export const updateNotifSettings = (data: { notifMessages?: boolean; notifyNewTasks?: boolean }) =>
request('/users/me', { method: 'PATCH', body: JSON.stringify(data) })
export const getNotifications = () => request('/notifications')
export const getUnreadCount = () => request<{ count: number }>('/notifications/count')
export const markRead = (id: string) => request(`/notifications/${id}/read`, { method: 'PATCH' })
export const markAllRead = () => request<{ ok: boolean }>('/notifications/read-all', { method: 'PATCH' })
export const getTaskInvite = (taskId: string) =>
request(`/notifications?type=task_invite&referenceId=${taskId}`)
export const dismissNotification = (id: string) =>
request<{ ok: boolean }>(`/notifications/${id}`, { method: 'DELETE' })
// ─── Referral ─────────────────────────────────────────────────────────────
export const getMyReferralInfo = () =>
request<{ referralCode: string | null; referralCount: number }>('/users/me/referral')
export const applyMyReferral = (refCode: string) =>
request<{ ok: boolean; skipped?: boolean }>('/users/me/referral/apply', {
method: 'POST',
body: JSON.stringify({ refCode }),
})
// ─── Reviews ──────────────────────────────────────────────────────────────
export const getUserReviews = (userId: string) =>
request>(`/reviews/user/${userId}`)
export const getUserRating = (userId: string) =>
request<{ avg: string | null }>(`/reviews/user/${userId}/rating`)
// ─── Categories ───────────────────────────────────────────────────────────
export const getCategories = () => request('/categories')
// ─── Locations ────────────────────────────────────────────────────────────
export const getLocations = () => request('/locations')
// ─── Favorites ────────────────────────────────────────────────────────────
export const getFavorites = () => request('/favorites')
export const addFavorite = (taskId: string) =>
request(`/favorites/${taskId}`, { method: 'POST' })
export const removeFavorite = (taskId: string) =>
request(`/favorites/${taskId}`, { method: 'DELETE' })
export const getSpecialistFavorites = () => request('/favorites/specialists')
export const addSpecialistFavorite = (specialistId: string) =>
request(`/favorites/specialists/${specialistId}`, { method: 'POST' })
export const removeSpecialistFavorite = (specialistId: string) =>
request(`/favorites/specialists/${specialistId}`, { method: 'DELETE' })
// ─── Admin ─────────────────────────────────────────────────────────────────
// ─── Admin Categories ──────────────────────────────────────────────────────
export const getAdminCategories = () =>
request('/admin/categories')
export const createCategory = (data: {
slug: string; icon?: string
namesEl: string; namesEn: string; namesRu: string; namesUk?: string
parentId?: string | null; order?: number
}) => request('/admin/categories', { method: 'POST', body: JSON.stringify(data) })
export const updateCategory = (id: string, data: {
slug?: string; icon?: string
namesEl?: string; namesEn?: string; namesRu?: string; namesUk?: string
parentId?: string | null; order?: number; isActive?: boolean
}) => request(`/admin/categories/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deleteCategory = (id: string) =>
request(`/admin/categories/${id}`, { method: 'DELETE' })
// ─── Admin Skills ──────────────────────────────────────────────────────────
export const getAdminSkills = (categorySlug: string) =>
request(`/admin/skills?categorySlug=${encodeURIComponent(categorySlug)}`)
export const createSkill = (data: {
categorySlug: string; nameEl: string; nameEn: string; nameRu: string; nameUk?: string; order?: number
}) => request('/admin/skills', { method: 'POST', body: JSON.stringify(data) })
export const updateSkill = (id: string, data: {
nameEl?: string; nameEn?: string; nameRu?: string; nameUk?: string; order?: number
}) => request(`/admin/skills/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deleteSkill = (id: string) =>
request(`/admin/skills/${id}`, { method: 'DELETE' })
export const getSkillDedup = (categorySlug?: string) =>
request(`/admin/skills/dedup${categorySlug ? `?categorySlug=${encodeURIComponent(categorySlug)}` : ''}`)
export const mergeSkillDuplicates = (keepId: string, deleteIds: string[]) =>
request<{ ok: boolean; merged: number }>('/admin/skills/dedup/merge', { method: 'POST', body: JSON.stringify({ keepId, deleteIds }) })
// ─── Admin ─────────────────────────────────────────────────────────────────
export const getAdminStats = () =>
request('/admin/stats')
export const getAdminUsers = (page = 1, q?: string, role?: string) => {
const p = new URLSearchParams({ page: String(page), limit: '20' })
if (q) p.set('q', q)
if (role) p.set('role', role)
return request(`/admin/users?${p}`)
}
export const deactivateUser = (id: string) =>
request(`/admin/users/${id}/deactivate`, { method: 'PATCH' })
export const activateUser = (id: string) =>
request(`/admin/users/${id}/activate`, { method: 'PATCH' })
export const changeUserRole = (id: string, role: string) =>
request(`/admin/users/${id}/role`, { method: 'PATCH', body: JSON.stringify({ role }) })
export const getAdminUser = (id: string) => request(`/admin/users/${id}`)
export const getAdminUserSpecialistCards = (userId: string) => request(`/admin/users/${userId}/specialist-cards`)
export const adminUpdateSpecialistCard = (id: string, data: {
title?: string; description?: string | null
skills?: string[]; categories?: string[]; locations?: string[]
publicationStatus?: 'pending' | 'active' | 'inactive'
isActive?: boolean; order?: number
}) => request(`/admin/specialist-cards/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const resolveAdminCategorySuggestion = (
cardId: string,
suggestionId: string,
data: {
action: 'map' | 'create' | 'reject'
categoryId?: string
slug?: string
parentId?: string | null
},
) => request(
`/admin/specialist-cards/${cardId}/category-suggestions/${suggestionId}`,
{ method: 'PATCH', body: JSON.stringify(data) },
)
export const getAdminSpecialistCards = (page = 1, status?: 'pending' | 'active' | 'inactive' | 'all' | 'review') => {
const p = new URLSearchParams({ page: String(page), limit: '20' })
if (status && status !== 'all') p.set('status', status)
return request(`/admin/specialist-cards?${p}`)
}
export const updateAdminUser = (id: string, data: {
firstName?: string; lastName?: string; phone?: string | null; bio?: string | null
role?: string; planId?: string | null; isActive?: boolean
locale?: string; languages?: string[]; personalSiteSlug?: string | null
showContactInfo?: boolean; notifyNewTasks?: boolean; notifMessages?: boolean
skills?: string[]; specialistCategories?: string[]; specialistLocations?: string[]
siteSettings?: Record
}) => request(`/admin/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const verifyAdminUserEmail = (id: string) =>
request(`/admin/users/${id}/verify-email`, { method: 'PATCH' })
export const getAdminTasks = (page = 1, q?: string, status?: string) => {
const p = new URLSearchParams({ page: String(page), limit: '20' })
if (q) p.set('q', q)
if (status) p.set('status', status)
return request(`/admin/tasks?${p}`)
}
export const cancelTaskAdmin = (id: string) =>
request(`/admin/tasks/${id}/cancel`, { method: 'PATCH' })
export const deleteTaskAdmin = (id: string) =>
request(`/admin/tasks/${id}`, { method: 'DELETE' })
export const getAdminReports = (page = 1, status?: 'pending' | 'reviewed' | 'dismissed' | 'all') => {
const p = new URLSearchParams({ page: String(page), limit: '20' })
if (status && status !== 'all') p.set('status', status)
return request(`/admin/reports?${p}`)
}
export const updateReportStatus = (id: string, status: 'reviewed' | 'dismissed') =>
request(`/admin/reports/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) })
export const getAdminAiLogs = (page = 1) =>
request(`/admin/ai-logs?page=${page}&limit=50`)
export const getAdminActivityLogs = (params: { page?: number; event?: string; userId?: string; dateFrom?: string; dateTo?: string } = {}) => {
const p = new URLSearchParams()
if (params.page) p.set('page', String(params.page))
if (params.event) p.set('event', params.event)
if (params.userId) p.set('userId', params.userId)
if (params.dateFrom) p.set('dateFrom', params.dateFrom)
if (params.dateTo) p.set('dateTo', params.dateTo)
p.set('limit', '50')
return request(`/admin/activity-logs?${p}`)
}
export const getAdminMailLogs = (params: { page?: number; event?: string; dateFrom?: string; dateTo?: string } = {}) => {
const p = new URLSearchParams()
if (params.page) p.set('page', String(params.page))
if (params.event) p.set('event', params.event)
if (params.dateFrom) p.set('dateFrom', params.dateFrom)
if (params.dateTo) p.set('dateTo', params.dateTo)
p.set('limit', '50')
return request(`/admin/mail-logs?${p}`)
}
export const getAdminNotifLogs = (params: { page?: number; type?: string; dateFrom?: string; dateTo?: string } = {}) => {
const p = new URLSearchParams()
if (params.page) p.set('page', String(params.page))
if (params.type) p.set('type', params.type)
if (params.dateFrom) p.set('dateFrom', params.dateFrom)
if (params.dateTo) p.set('dateTo', params.dateTo)
p.set('limit', '50')
return request(`/admin/notification-logs?${p}`)
}
export const getAdminNotifStats = () => request<{ day: string; count: number }[]>('/admin/notification-stats')
export const getAdminMailStats = () => request<{ day: string; count: number }[]>('/admin/mail-stats')
// ─── Admin Referrals ──────────────────────────────────────────────────────
export type ReferralReferee = { id: string; firstName: string | null; lastName: string | null; email: string; planId: string; daysAdded: number; expiresAt: string; createdAt: string }
export type ReferralTreeNode = { referrer: { id: string; firstName: string | null; lastName: string | null; email: string; referralCode: string | null } | undefined; referees: ReferralReferee[] }
export type AdminReferralsData = { totalReferrers: number; totalReferrals: number; activeRewards: number; tree: ReferralTreeNode[] }
export const getAdminReferrals = () => request('/admin/referrals')
export type AdminPushPayload = {
title: string
body?: string
type?: string
referenceId?: string
data?: Record
}
export type AdminPushUserResult = {
ok: boolean
targetUserId: string
sent: number
invalidTokens: number
tokensFound?: number
reason?: 'fcm_not_configured' | 'fcm_auth_failed' | 'no_tokens' | 'fcm_send_failed' | null
failureKey?: string | null
failureMessage?: string | null
}
export type AdminPushBroadcastResult = {
ok: boolean
usersTotal: number
usersWithSent: number
pushesSent: number
invalidTokens: number
usersWithoutTokens?: number
usersConfigBlocked?: number
usersAuthFailed?: number
usersAllFailed?: number
failedReasonStats?: Record
failedReasonSample?: string | null
failedUsers: string[]
}
export const adminSendPushToUser = (userId: string, data: AdminPushPayload) =>
request(`/admin/push/user/${userId}`, {
method: 'POST',
body: JSON.stringify(data),
})
export const adminBroadcastPush = (
data: AdminPushPayload & { onlyActive?: boolean },
) =>
request('/admin/push/broadcast', {
method: 'POST',
body: JSON.stringify(data),
})
// ─── Admin Locations ───────────────────────────────────────────────────────
export const getAdminLocations = () => request('/admin/locations')
export const createLocation = (data: {
slug: string; nameEl: string; nameEn: string; nameRu: string; nameUk?: string
parentId?: string | null; order?: number
}) => request('/admin/locations', { method: 'POST', body: JSON.stringify(data) })
export const updateLocation = (id: string, data: {
slug?: string; nameEl?: string; nameEn?: string; nameRu?: string; nameUk?: string
parentId?: string | null; order?: number; isActive?: boolean
}) => request(`/admin/locations/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deleteLocation = (id: string) =>
request(`/admin/locations/${id}`, { method: 'DELETE' })
// ─── Admin Plans ───────────────────────────────────────────────────────────
export const getAdminPlans = () => request('/admin/plans')
export const createPlan = (data: {
name: string; description?: string; price?: string; currency?: string
maxTasks?: number | null; maxOffers?: number | null; maxCards?: number | null
durationDays?: number | null; features?: string[]
isDefault?: boolean; isActive?: boolean; order?: number
}) => request('/admin/plans', { method: 'POST', body: JSON.stringify(data) })
export const updatePlan = (id: string, data: {
name?: string; description?: string; price?: string; currency?: string
maxTasks?: number | null; maxOffers?: number | null; maxCards?: number | null
durationDays?: number | null; features?: string[]
isDefault?: boolean; isActive?: boolean; order?: number
}) => request(`/admin/plans/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deletePlan = (id: string) =>
request(`/admin/plans/${id}`, { method: 'DELETE' })
// ─── Admin Settings ────────────────────────────────────────────────────────
export const getAdminSettings = () =>
request>('/admin/settings')
export const updateAdminSettings = (data: Record) =>
request<{ ok: boolean }>('/admin/settings', { method: 'PUT', body: JSON.stringify(data) })
export const getPublicSettings = () =>
request<{ showPricing: boolean }>('/admin/public-settings')
export const getAdminEmailTemplates = () =>
request<{ key: string; subject: string; body: string; updatedAt: string }[]>('/admin/settings/templates')
export const updateAdminEmailTemplate = (key: string, data: { subject: string; body: string }) =>
request(`/admin/settings/templates/${key}`, { method: 'PUT', body: JSON.stringify(data) })
export const translateAdminEmailTemplate = (data: { subject: string; body: string; fromLocale: string }) =>
request>('/admin/settings/templates/translate', { method: 'POST', body: JSON.stringify(data) })
export const sendTestEmail = () =>
request<{ ok: boolean; to: string }>('/admin/settings/test-email', { method: 'POST' })
export const testTelegramConnection = (botToken: string, chatId: string) =>
fetch(`${BASE}/api/admin/settings/test-telegram`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botToken, chatId }),
}).then((r) => r.json() as Promise<{ ok: boolean; botName?: string; error?: string }>)
export const verifyTelegramToken = (botToken: string) =>
fetch(`${BASE}/api/admin/settings/verify-telegram-token`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botToken }),
}).then((r) => r.json() as Promise<{ ok: boolean; botName?: string; error?: string }>)
// ─── Admin Translations ────────────────────────────────────────────────────
export type TranslationTableStats = {
name: string
label: string
total: number
missing: Record
}
export const getTranslationsStats = () =>
request<{ tables: TranslationTableStats[] }>('/admin/translations/stats')
export const getTranslationsMissing = (table: string, missingLang: string, page = 1, limit = 50) =>
request<{ rows: any[]; total: number; page: number; limit: number }>(
`/admin/translations/${table}?missingLang=${missingLang}&page=${page}&limit=${limit}`,
)
export const saveTranslations = (table: string, items: { id: string; lang: string; value: string }[]) =>
request<{ saved: number }>(`/admin/translations/${table}/save`, {
method: 'POST',
body: JSON.stringify({ items }),
})
export const autoTranslate = (table: string, missingLang: string, fromLang: string, batchSize = 500) =>
request<{ translated: number; failed: number; total: number }>('/admin/translations/auto-translate', {
method: 'POST',
body: JSON.stringify({ table, missingLang, fromLang, batchSize }),
})
export const autoTranslateTasks = (batchSize = 500) =>
request<{ translated: number; failed: number; total: number }>('/admin/translations/auto-translate-tasks', {
method: 'POST',
body: JSON.stringify({ batchSize }),
})
export const getTasksTranslationPreview = (lang: string, limit = 50) =>
request<{ lang: string; rows: Array<{ id: string; title: string; originalLocale: string; titleEl: string | null; titleEn: string | null; titleRu: string | null; titleUk: string | null }> }>(
`/admin/translations/tasks-preview?lang=${lang}&limit=${limit}`,
)
export const autoTranslateEmailTemplates = (missingLang: string, fromLang = 'el') =>
request<{ translated: number; failed: number; total: number }>('/admin/translations/auto-translate-templates', {
method: 'POST',
body: JSON.stringify({ missingLang, fromLang }),
})
// ─── Availability ──────────────────────────────────────────────────────────
export const getMyAvailability = (month?: string) =>
request(`/availability/my${month ? `?month=${month}` : ''}`)
export const setAvailability = (date: string, status: 'available' | 'busy' | null, note?: string) =>
request('/availability/set', { method: 'POST', body: JSON.stringify({ date, status, note }) })
export const setAvailabilityRange = (from: string, to: string, status: 'available' | 'busy' | null) =>
request('/availability/set-range', { method: 'POST', body: JSON.stringify({ from, to, status }) })
export const getSpecialistAvailability = (userId: string, month?: string) =>
request>(`/availability/user/${userId}${month ? `?month=${month}` : ''}`)
// ─── Portfolio ────────────────────────────────────────────────────────────
export type PortfolioItem = {
id: string
specialistId?: string
imageUrl: string
title: string | null
description: string | null
order: number
createdAt: string
}
export const getMyPortfolio = () =>
request('/portfolio/my')
export const addPortfolioItem = (formData: FormData) =>
fetch(`${BASE}/api/portfolio`, {
method: 'POST',
credentials: 'include',
body: formData,
}).then(async (res) => {
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || 'Upload failed')
}
return res.json() as Promise
})
export const updatePortfolioItem = (id: string, data: { title?: string | null; description?: string | null; order?: number }) =>
request(`/portfolio/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deletePortfolioItem = (id: string) =>
request<{ ok: boolean }>(`/portfolio/${id}`, { method: 'DELETE' })
export const getSpecialistPortfolio = (userId: string) =>
request(`/portfolio/user/${userId}`)
// ─── Specialist Cards ─────────────────────────────────────────────────────
export type SpecialistCard = {
id: string
specialistId: string
title: string
description: string | null
descriptionEl: string | null
descriptionEn: string | null
descriptionRu: string | null
descriptionUk: string | null
skills: string[] | null
categories: string[] | null
locations: string[] | null
publicationStatus?: 'pending' | 'active' | 'inactive'
isActive: boolean
order: number
createdAt: string
updatedAt: string
portfolio: PortfolioItem[]
}
export const getMySpecialistCards = () =>
request<{ cards: SpecialistCard[]; maxCards: number | null }>('/specialist-cards/my')
export const createSpecialistCard = (data: { title: string; description?: string; skills?: string[]; categories?: string[]; locations?: string[] }) =>
request('/specialist-cards', { method: 'POST', body: JSON.stringify(data) })
export const updateSpecialistCard = (id: string, data: { title?: string; description?: string | null; descriptionEl?: string | null; descriptionEn?: string | null; descriptionRu?: string | null; descriptionUk?: string | null; descriptionLocale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean; skills?: string[]; categories?: string[]; locations?: string[]; publicationStatus?: 'pending' | 'active' | 'inactive'; isActive?: boolean; order?: number }) =>
request(`/specialist-cards/${id}`, { method: 'PATCH', body: JSON.stringify(data) })
export const saveSiteSeo = (data: { seoTitle?: string; seoDescription?: string; locale: 'el' | 'en' | 'ru' | 'uk' }) =>
request<{ ok: boolean }>('/users/me/site-seo', { method: 'POST', body: JSON.stringify(data) })
export const deleteSpecialistCard = (id: string) =>
request<{ ok: boolean }>(`/specialist-cards/${id}`, { method: 'DELETE' })
export const getSpecialistCards = (userId: string, locale?: string) =>
request(`/specialist-cards/user/${userId}${locale ? `?locale=${encodeURIComponent(locale)}` : ''}`)
// ─── Price List ──────────────────────────────────────────────────────────
export type PriceListItem = {
id: string
groupId: string
cardId: string
specialistId: string
name: string
nameEl: string | null
nameEn: string | null
nameRu: string | null
nameUk: string | null
description: string | null
descriptionEl: string | null
descriptionEn: string | null
descriptionRu: string | null
descriptionUk: string | null
price: string | null
unit: string | null
unitEl: string | null
unitEn: string | null
unitRu: string | null
unitUk: string | null
originalLocale: 'el' | 'en' | 'ru' | 'uk'
order: number
createdAt: string
}
export type PriceListGroup = {
id: string
cardId: string
specialistId: string
title: string
titleEl: string | null
titleEn: string | null
titleRu: string | null
titleUk: string | null
originalLocale: 'el' | 'en' | 'ru' | 'uk'
order: number
createdAt: string
items: PriceListItem[]
}
export const getPriceList = (cardId: string, locale?: string) =>
request(`/price-list/${cardId}${locale ? `?locale=${encodeURIComponent(locale)}` : ''}`)
export const createPriceListGroup = (cardId: string, data: { title: string; locale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean }) =>
request(`/price-list/${cardId}/groups`, { method: 'POST', body: JSON.stringify(data) })
export const updatePriceListGroup = (groupId: string, data: { title?: string; locale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean; order?: number }) =>
request(`/price-list/groups/${groupId}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deletePriceListGroup = (groupId: string) =>
request<{ ok: boolean }>(`/price-list/groups/${groupId}`, { method: 'DELETE' })
export const createPriceListItem = (groupId: string, data: { name: string; description?: string | null; price?: string | null; unit?: string | null; locale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean }) =>
request(`/price-list/groups/${groupId}/items`, { method: 'POST', body: JSON.stringify(data) })
export const updatePriceListItem = (itemId: string, data: { name?: string; description?: string | null; price?: string | null; unit?: string | null; locale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean; order?: number }) =>
request(`/price-list/items/${itemId}`, { method: 'PATCH', body: JSON.stringify(data) })
export const deletePriceListItem = (itemId: string) =>
request<{ ok: boolean }>(`/price-list/items/${itemId}`, { method: 'DELETE' })
export type SkillSuggestion = {
id: string
categorySlug: string
names: { el: string; en: string; ru: string; uk?: string }
order: number
}
export const getSkillSuggestions = (categorySlugs?: string[]) => {
const params = categorySlugs?.length
? '?' + categorySlugs.map((s) => `category=${encodeURIComponent(s)}`).join('&')
: ''
return request(`/skill-suggestions${params}`)
}
// ─── Payments ──────────────────────────────────────────────────────────────
export type Payment = {
id: string
userId: string
orderRef: string
amount: string
currency: string
type: 'balance' | 'plan'
status: 'pending' | 'success' | 'failed' | 'cancelled'
description: string | null
transactionId: string | null
createdAt: string
updatedAt: string
}
export type BalanceTransaction = {
id: string
userId: string
adminUserId: string | null
paymentId: string | null
kind: 'topup' | 'plan_purchase' | 'admin_topup' | 'admin_adjustment'
direction: 'credit' | 'debit'
amount: string
balanceBefore: string
balanceAfter: string
currency: string
description: string | null
metadata: Record | null
createdAt: string
}
export type AdminBalanceTransaction = BalanceTransaction & {
userEmail: string | null
userName: string | null
}
export type PlanHistoryEntry = {
id: string
userId: string
event: 'activated' | 'renewed' | 'downgraded' | 'referral_reward' | 'admin_change' | 'expired'
planId: string
planName: string
previousPlanId: string | null
previousPlanName: string | null
expiresAt: string | null
daysAdded: number | null
referralRewardId: string | null
note: string | null
createdAt: string
}
export type TopUpInitResponse = {
url: string
params: Record
}
export const initiateTopUp = (amount: number) =>
request('/payments/topup/init', {
method: 'POST',
body: JSON.stringify({ amount }),
})
export const getPaymentHistory = () =>
request('/payments/history')
export const getBalanceHistory = (limit = 50) =>
request(`/payments/balance-history?limit=${limit}`)
export const getPlanHistory = (limit = 50) =>
request(`/users/me/plan-history?limit=${limit}`)
export const getAdminBalanceHistory = (params: { page?: number; userId?: string; kind?: string } = {}) => {
const p = new URLSearchParams()
if (params.page) p.set('page', String(params.page))
if (params.userId) p.set('userId', params.userId)
if (params.kind) p.set('kind', params.kind)
p.set('limit', '50')
return request<{ data: AdminBalanceTransaction[]; total: number; page: number; limit: number }>(`/admin/balance-history?${p}`)
}
export const adminTopUpUserBalance = (id: string, data: { amount: string | number; description?: string }) =>
request<{ user: any; transaction: BalanceTransaction }>(`/admin/users/${id}/balance/topup`, {
method: 'POST',
body: JSON.stringify(data),
})
export const adminWithdrawUserBalance = (id: string, data: { amount: string | number; description?: string }) =>
request<{ user: any; transaction: BalanceTransaction }>(`/admin/users/${id}/balance/withdraw`, {
method: 'POST',
body: JSON.stringify(data),
})
// ─── Plans ─────────────────────────────────────────────────────────────────
export type PlanTier = 'free' | 'pro' | 'ultimate'
export type PlanRole = 'customer' | 'specialist'
export type Plan = {
id: string
name: string
description: string | null
role: PlanRole
tier: PlanTier
price: string
oldPrice?: string | null
durationDays: number | null
maxMessagesPerDay: number | null
maxOrdersPerDay: number | null
maxCards: number | null
maxSkills: number | null
maxPortfolioItems: number | null
canContactFreePlan: boolean
canContactProPlan: boolean
canContactAll: boolean
canShowContactInfo: boolean
canViewPhone: boolean
canUploadVideo: boolean
hasFavorites: boolean
hasGoogleCalendar: boolean
hasVerifiedBadge: boolean
highlightedReviews: boolean
hasPersonalSite: boolean
hasAutoResponse: boolean
hasAutoMatch: boolean
hasCanHelpNowStatus: boolean
hasNeedHelpStatus: boolean
notifyNewTasks: boolean
searchBoost: number
offersMultiplier: string
receiveInstantDispatchFree: boolean
receiveInstantDispatchPro: boolean
isActive: boolean
order: number
}
export type DashboardData = {
plan: { id: string; name: string; role: PlanRole; tier: PlanTier; price: string } | null
planExpiration: { expiresAt: string; daysLeft: number } | null
usage: {
messagesLeft: number | null
ordersLeft: number | null
messagesSent: number
ordersMade: number
}
stats: Record
features: Record | null
googleCalendar: { connected: boolean; email?: string } | null
personalSiteSlug: string | null
showPricing: boolean
}
export type PlanPreview = {
type: 'free' | 'upgrade' | 'downgrade' | 'same'
effectivePrice: string
prorated: string
currentBalance: string
canPayFromBalance: boolean
scheduledDate: string | null
}
export const previewPlanChange = (planId: string) =>
request('/payments/plan/preview', {
method: 'POST',
body: JSON.stringify({ planId }),
})
export const getPlans = (role?: PlanRole) => {
const qs = role ? `?role=${role}` : ''
return request(`/plans${qs}`)
}
export const getMyDashboard = () => request('/dashboard/me')
export const getDailyUsage = () =>
request<{ messagesLeft: number | null; ordersLeft: number | null; messagesSent: number; ordersMade: number }>('/plans/daily-usage')
export const purchasePlan = (planId: string, payFromBalance = true) =>
request<{
success?: boolean
scheduled?: boolean
activeUntil?: string
method?: string
effectivePrice?: string
expiresAt?: string
url?: string
params?: Record
}>(
'/payments/plan/purchase',
{ method: 'POST', body: JSON.stringify({ planId, payFromBalance }) }
)
// ─── Statuses ──────────────────────────────────────────────────────────────
export const getMyStatuses = () =>
request>('/statuses/me')
export const toggleStatus = (statusType: string, active: boolean) =>
request<{ id: string; isActive: boolean; statusType: string }>('/statuses/toggle', {
method: 'POST',
body: JSON.stringify({ statusType, active }),
})
// ─── Auto-response ─────────────────────────────────────────────────────────
export type AutoResponseSettings = {
enabled: boolean
categories: string[] | null
locations: string[] | null
minBudget: number | null
maxBudget: number | null
defaultPrice: number | null
defaultMessage: string | null
maxAutoOffersPerDay: number | null
}
export const getAutoResponseSettings = () =>
request('/auto-response')
export const updateAutoResponseSettings = (data: Partial) =>
request('/auto-response', { method: 'PUT', body: JSON.stringify(data) })
// ─── FOMO ──────────────────────────────────────────────────────────────────
export const getSpecialistFomoDashboard = () =>
request('/fomo/specialist')
export const getCustomerFomoDashboard = () =>
request('/fomo/customer')
export const getFomoRecommendations = () =>
request('/fomo/recommendations')
// ─── Auto-match ────────────────────────────────────────────────────────────
export const getAutoMatchResults = (taskId: string) =>
request<{ matches: any[] }>(`/auto-match/${taskId}`)
export const runAutoMatch = (taskId: string) =>
request<{ matches: any[] }>(`/auto-match/${taskId}`, { method: 'POST' })
// ─── Google Calendar ───────────────────────────────────────────────────────
export const getGoogleCalendarStatus = () =>
request<{ connected: boolean; email?: string; googleUserId?: string }>('/google/calendar/status')
export const disconnectGoogleCalendar = () =>
request<{ ok: boolean }>('/google/calendar/disconnect', { method: 'DELETE' })
export const importFromGoogleCalendar = () =>
request<{ ok: boolean; imported: number }>('/google/calendar/import', { method: 'POST' })
export const getGoogleCalendarConnectUrl = () =>
`${BASE}/api/google/calendar/connect`
// ─── Personal site ─────────────────────────────────────────────────────────
export const getPublicSiteProfile = (slug: string) =>
request(`/users/site/${slug}`)
export const getSiteStats = () =>
request<{ days: { date: string; views: number; unique: number; messages: number }[] }>('/users/me/site-stats')
export const trackSiteMessage = (slug: string) =>
request<{ ok: boolean }>(`/users/site/${slug}/track-message`, { method: 'POST' }).catch(() => {})
// ─── Task stats (FOMO) ─────────────────────────────────────────────────────
export const getTaskStats = (taskId: string) =>
request<{ views: number; offers: number; uniqueSpecialists: number }>(`/tasks/${taskId}/stats`)
// ─── Support tickets ───────────────────────────────────────────────────────
export const sendPublicSupportMessage = (data: { name: string; email: string; subject: string; body: string; captchaToken: string }) =>
request<{ ok: boolean }>('/support/public', { method: 'POST', body: JSON.stringify(data) })
export const createSupportTicket = (data: { type: string; subject: string; body: string }) =>
request('/support', { method: 'POST', body: JSON.stringify(data) })
export const getMySupportTickets = (page = 1) =>
request<{ data: any[]; total: number; page: number; limit: number }>(`/support/my?page=${page}`)
export const getAdminSupportTickets = (params?: Record) =>
request<{ data: any[]; total: number; page: number; limit: number }>(
`/support${params ? '?' + new URLSearchParams(params) : ''}`,
)
export const replyToSupportTicket = (id: string, data: { adminReply: string; status?: string }) =>
request(`/support/${id}/reply`, { method: 'PATCH', body: JSON.stringify(data) })
export const setSupportTicketStatus = (id: string, status: string) =>
request(`/support/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) })