/
opt
/
canhelp
/
apps
/
web
/
src
/
lib
/
/opt/canhelp/apps/web/src/lib
mkdir
upload
Name
Size
Mode
Actions
api.ts
44017
0644
edit
dl
rm
auth.ts
312
0644
edit
dl
rm
formatLastSeen.ts
643
0644
edit
dl
rm
formatName.ts
865
0644
edit
dl
rm
taskLocale.ts
941
0644
edit
dl
rm
translations.ts
365788
0644
edit
dl
rm
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<T>(path: string, init?: RequestInit): Promise<T> { 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<string, string>) => { const qs = params ? '?' + new URLSearchParams(params) : '' return request<Paginated<Task>>(`/tasks${qs}`) } export const getTaskBudgetRange = (params?: Record<string, string>) => { const qs = params ? '?' + new URLSearchParams(params) : '' return request<{ min: number; max: number }>(`/tasks/budget-range${qs}`) } export const searchTasks = (params: Record<string, string>) => request<Paginated<Task>>(`/tasks/search?${new URLSearchParams(params)}`) export const getMyTasks = () => request<Task[]>('/tasks/my') export const getTasksForSpecialist = (params?: Record<string, string>) => { 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<Record<string, number>>('/tasks/counts-by-category') export const getTask = (id: string) => request<{ task: Task; customer: any }>(`/tasks/${id}`) export const createTask = (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }) export const updateTask = (id: string, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const completeTask = (id: string) => request<Task>(`/tasks/${id}/complete`, { method: 'PATCH' }) export const cancelTask = (id: string) => request<Task>(`/tasks/${id}/cancel`, { method: 'PATCH' }) // ─── Offers ──────────────────────────────────────────────────────────────── export const getOffers = (taskId: string) => request<Array<{ offer: Offer; specialist: any }>>(`/offers?taskId=${taskId}`) export const getMyOffers = () => request<Array<{ offer: Offer; task: any }>>('/offers/my') export const createOffer = (data: { taskId: string; price: number; message?: string }) => request<Offer>('/offers', { method: 'POST', body: JSON.stringify(data) }) export const acceptOffer = (id: string) => request<Offer>(`/offers/${id}/accept`, { method: 'PATCH' }) export const declineOffer = (id: string) => request<Offer>(`/offers/${id}/decline`, { method: 'PATCH' }) export const getUserProfile = (id: string, locale?: string) => request<any>(`/users/${id}/profile${locale ? `?locale=${encodeURIComponent(locale)}` : ''}`) export const getSpecialists = (params?: Record<string, string>) => { 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<any>('/reviews', { method: 'POST', body: JSON.stringify(data) }) export const getTaskReviews = (taskId: string) => request<any[]>(`/reviews/task/${taskId}`) export const getMyReviews = () => request<Array<{ review: any; author: any }>>('/reviews/my') // ─── Chat ────────────────────────────────────────────────────────────────── export const getChatRooms = () => request<Array<{ room: ChatRoom; task: { id: string; title: string } | null; otherUser: { id: string; name: string; firstName?: string; lastName?: string; image?: string } | null; lastMessage?: { body: string; createdAt: string; translations?: Record<string, string> | 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<Array<{ message: Message & { imageUrl?: string | null; translations?: Record<string, string> | 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<Message>('/chat/messages', { method: 'POST', body: JSON.stringify({ roomId, body }) }) // ─── Notifications ──────────────────────────────────────────────────────── export const updateNotifSettings = (data: { notifMessages?: boolean; notifyNewTasks?: boolean }) => request<any>('/users/me', { method: 'PATCH', body: JSON.stringify(data) }) export const getNotifications = () => request<Notification[]>('/notifications') export const getUnreadCount = () => request<{ count: number }>('/notifications/count') export const markRead = (id: string) => request<Notification>(`/notifications/${id}/read`, { method: 'PATCH' }) export const markAllRead = () => request<{ ok: boolean }>('/notifications/read-all', { method: 'PATCH' }) export const getTaskInvite = (taskId: string) => request<Notification[]>(`/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<Array<{ review: Review; author: any }>>(`/reviews/user/${userId}`) export const getUserRating = (userId: string) => request<{ avg: string | null }>(`/reviews/user/${userId}/rating`) // ─── Categories ─────────────────────────────────────────────────────────── export const getCategories = () => request<Category[]>('/categories') // ─── Locations ──────────────────────────────────────────────────────────── export const getLocations = () => request<any[]>('/locations') // ─── Favorites ──────────────────────────────────────────────────────────── export const getFavorites = () => request<any[]>('/favorites') export const addFavorite = (taskId: string) => request<any>(`/favorites/${taskId}`, { method: 'POST' }) export const removeFavorite = (taskId: string) => request<any>(`/favorites/${taskId}`, { method: 'DELETE' }) export const getSpecialistFavorites = () => request<any[]>('/favorites/specialists') export const addSpecialistFavorite = (specialistId: string) => request<any>(`/favorites/specialists/${specialistId}`, { method: 'POST' }) export const removeSpecialistFavorite = (specialistId: string) => request<any>(`/favorites/specialists/${specialistId}`, { method: 'DELETE' }) // ─── Admin ───────────────────────────────────────────────────────────────── // ─── Admin Categories ────────────────────────────────────────────────────── export const getAdminCategories = () => request<any[]>('/admin/categories') export const createCategory = (data: { slug: string; icon?: string namesEl: string; namesEn: string; namesRu: string; namesUk?: string parentId?: string | null; order?: number }) => request<any>('/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<any>(`/admin/categories/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteCategory = (id: string) => request<any>(`/admin/categories/${id}`, { method: 'DELETE' }) // ─── Admin Skills ────────────────────────────────────────────────────────── export const getAdminSkills = (categorySlug: string) => request<any[]>(`/admin/skills?categorySlug=${encodeURIComponent(categorySlug)}`) export const createSkill = (data: { categorySlug: string; nameEl: string; nameEn: string; nameRu: string; nameUk?: string; order?: number }) => request<any>('/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<any>(`/admin/skills/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteSkill = (id: string) => request<any>(`/admin/skills/${id}`, { method: 'DELETE' }) export const getSkillDedup = (categorySlug?: string) => request<any[][]>(`/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<any>('/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<any>(`/admin/users?${p}`) } export const deactivateUser = (id: string) => request<any>(`/admin/users/${id}/deactivate`, { method: 'PATCH' }) export const activateUser = (id: string) => request<any>(`/admin/users/${id}/activate`, { method: 'PATCH' }) export const changeUserRole = (id: string, role: string) => request<any>(`/admin/users/${id}/role`, { method: 'PATCH', body: JSON.stringify({ role }) }) export const getAdminUser = (id: string) => request<any>(`/admin/users/${id}`) export const getAdminUserSpecialistCards = (userId: string) => request<any[]>(`/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<any>(`/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<any>( `/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<any>(`/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<string, boolean | undefined> }) => request<any>(`/admin/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const verifyAdminUserEmail = (id: string) => request<any>(`/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<any>(`/admin/tasks?${p}`) } export const cancelTaskAdmin = (id: string) => request<any>(`/admin/tasks/${id}/cancel`, { method: 'PATCH' }) export const deleteTaskAdmin = (id: string) => request<any>(`/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<any>(`/admin/reports?${p}`) } export const updateReportStatus = (id: string, status: 'reviewed' | 'dismissed') => request<any>(`/admin/reports/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) }) export const getAdminAiLogs = (page = 1) => request<any>(`/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<any>(`/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<any>(`/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<any>(`/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<AdminReferralsData>('/admin/referrals') export type AdminPushPayload = { title: string body?: string type?: string referenceId?: string data?: Record<string, string> } 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<string, number> failedReasonSample?: string | null failedUsers: string[] } export const adminSendPushToUser = (userId: string, data: AdminPushPayload) => request<AdminPushUserResult>(`/admin/push/user/${userId}`, { method: 'POST', body: JSON.stringify(data), }) export const adminBroadcastPush = ( data: AdminPushPayload & { onlyActive?: boolean }, ) => request<AdminPushBroadcastResult>('/admin/push/broadcast', { method: 'POST', body: JSON.stringify(data), }) // ─── Admin Locations ─────────────────────────────────────────────────────── export const getAdminLocations = () => request<any[]>('/admin/locations') export const createLocation = (data: { slug: string; nameEl: string; nameEn: string; nameRu: string; nameUk?: string parentId?: string | null; order?: number }) => request<any>('/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<any>(`/admin/locations/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deleteLocation = (id: string) => request<any>(`/admin/locations/${id}`, { method: 'DELETE' }) // ─── Admin Plans ─────────────────────────────────────────────────────────── export const getAdminPlans = () => request<any[]>('/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<any>('/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<any>(`/admin/plans/${id}`, { method: 'PATCH', body: JSON.stringify(data) }) export const deletePlan = (id: string) => request<any>(`/admin/plans/${id}`, { method: 'DELETE' }) // ─── Admin Settings ──────────────────────────────────────────────────────── export const getAdminSettings = () => request<Record<string, string>>('/admin/settings') export const updateAdminSettings = (data: Record<string, string>) => 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<any>(`/admin/settings/templates/${key}`, { method: 'PUT', body: JSON.stringify(data) }) export const translateAdminEmailTemplate = (data: { subject: string; body: string; fromLocale: string }) => request<Record<string, { subject: string; body: string }>>('/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<string, number> } 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<any[]>(`/availability/my${month ? `?month=${month}` : ''}`) export const setAvailability = (date: string, status: 'available' | 'busy' | null, note?: string) => request<any>('/availability/set', { method: 'POST', body: JSON.stringify({ date, status, note }) }) export const setAvailabilityRange = (from: string, to: string, status: 'available' | 'busy' | null) => request<any>('/availability/set-range', { method: 'POST', body: JSON.stringify({ from, to, status }) }) export const getSpecialistAvailability = (userId: string, month?: string) => request<Array<{ date: string; status: 'available' | 'busy' }>>(`/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<PortfolioItem[]>('/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<PortfolioItem> }) export const updatePortfolioItem = (id: string, data: { title?: string | null; description?: string | null; order?: number }) => request<PortfolioItem>(`/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<PortfolioItem[]>(`/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<SpecialistCard>('/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<SpecialistCard>(`/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<SpecialistCard[]>(`/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<PriceListGroup[]>(`/price-list/${cardId}${locale ? `?locale=${encodeURIComponent(locale)}` : ''}`) export const createPriceListGroup = (cardId: string, data: { title: string; locale?: 'el' | 'en' | 'ru' | 'uk'; autoTranslate?: boolean }) => request<PriceListGroup>(`/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<PriceListGroup>(`/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<PriceListItem>(`/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<PriceListItem>(`/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<SkillSuggestion[]>(`/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<string, unknown> | 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<string, string> } export const initiateTopUp = (amount: number) => request<TopUpInitResponse>('/payments/topup/init', { method: 'POST', body: JSON.stringify({ amount }), }) export const getPaymentHistory = () => request<Payment[]>('/payments/history') export const getBalanceHistory = (limit = 50) => request<BalanceTransaction[]>(`/payments/balance-history?limit=${limit}`) export const getPlanHistory = (limit = 50) => request<PlanHistoryEntry[]>(`/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<string, number> features: Record<string, boolean | number> | 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<PlanPreview>('/payments/plan/preview', { method: 'POST', body: JSON.stringify({ planId }), }) export const getPlans = (role?: PlanRole) => { const qs = role ? `?role=${role}` : '' return request<Plan[]>(`/plans${qs}`) } export const getMyDashboard = () => request<DashboardData>('/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<string, string> }>( '/payments/plan/purchase', { method: 'POST', body: JSON.stringify({ planId, payFromBalance }) } ) // ─── Statuses ────────────────────────────────────────────────────────────── export const getMyStatuses = () => request<Array<{ statusType: string; isActive: boolean; expiresAt: string | null }>>('/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<AutoResponseSettings>('/auto-response') export const updateAutoResponseSettings = (data: Partial<AutoResponseSettings>) => request<AutoResponseSettings>('/auto-response', { method: 'PUT', body: JSON.stringify(data) }) // ─── FOMO ────────────────────────────────────────────────────────────────── export const getSpecialistFomoDashboard = () => request<any>('/fomo/specialist') export const getCustomerFomoDashboard = () => request<any>('/fomo/customer') export const getFomoRecommendations = () => request<any>('/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<any>(`/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<any>('/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<string, string>) => 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<any>(`/support/${id}/reply`, { method: 'PATCH', body: JSON.stringify(data) }) export const setSupportTicketStatus = (id: string, status: string) => request<any>(`/support/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status }) })
Save
cmd:
run