/opt/canhelp/apps/web/src/lib
NameSizeModeActions
api.ts440170644editdlrm
auth.ts3120644editdlrm
formatLastSeen.ts6430644editdlrm
formatName.ts8650644editdlrm
taskLocale.ts9410644editdlrm
translations.ts3657880644editdlrm
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 }) })