// API Configuration export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001' // API Endpoints export const API_ENDPOINTS = { // Auth LOGIN: '/api/login', REFRESH: '/api/refresh', LOGOUT: '/api/logout', ME: '/api/me', // User Status & Settings USER_STATUS: '/api/user/status', USER_STATUS_PROGRESS: '/api/user/status-progress', USER_SETTINGS: '/api/user/settings', // Email Verification SEND_VERIFICATION_EMAIL: '/api/send-verification-email', VERIFY_EMAIL_CODE: '/api/verify-email-code', // Document Upload UPLOAD_PERSONAL_ID: '/api/upload/personal-id', UPLOAD_COMPANY_ID: '/api/upload/company-id', // Profile Completion COMPLETE_PERSONAL_PROFILE: '/api/profile/personal/complete', COMPLETE_COMPANY_PROFILE: '/api/profile/company/complete', // Contract Signing UPLOAD_PERSONAL_CONTRACT: '/api/upload/contract/personal', UPLOAD_COMPANY_CONTRACT: '/api/upload/contract/company', // Documents USER_DOCUMENTS: '/api/users/:id/documents', // Admin ADMIN_USERS: '/api/admin/users/:id/full', ADMIN_USER_DETAILED: '/api/admin/users/:id/detailed', ADMIN_USER_STATS: '/api/admin/user-stats', ADMIN_USER_LIST: '/api/admin/user-list', ADMIN_VERIFICATION_PENDING: '/api/admin/verification-pending-users', ADMIN_UNVERIFIED_USERS: '/api/admin/unverified-users', ADMIN_VERIFY_USER: '/api/admin/verify-user/:id', ADMIN_ARCHIVE_USER: '/api/admin/archive-user/:id', ADMIN_UNARCHIVE_USER: '/api/admin/unarchive-user/:id', ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id', ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id', ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id', // Pools (admin) ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members', // Coffee products (admin) ADMIN_COFFEE_LIST: '/api/admin/coffee', ADMIN_COFFEE_CREATE: '/api/admin/coffee', ADMIN_COFFEE_UPDATE: '/api/admin/coffee/:id', ADMIN_COFFEE_SET_STATE: '/api/admin/coffee/:id/state', ADMIN_COFFEE_DELETE: '/api/admin/coffee/:id', // Contract preview (admin) – matches backend route ADMIN_CONTRACT_PREVIEW: '/api/admin/contracts/:id/preview', ADMIN_CONTRACT_DOCS: '/api/admin/contracts/:id/documents', ADMIN_CONTRACT_MOVE: '/api/admin/contracts/:id/move', ADMIN_CONTRACT_FILES: '/api/admin/contracts/:id/files', } // API Helper Functions export class ApiClient { static async makeRequest( endpoint: string, options: RequestInit = {}, token?: string ): Promise { const url = `${API_BASE_URL}${endpoint}` const defaultHeaders: HeadersInit = { 'Content-Type': 'application/json', } if (token) { defaultHeaders['Authorization'] = `Bearer ${token}` } const config: RequestInit = { ...options, headers: { ...defaultHeaders, ...options.headers, }, } return fetch(url, config) } static async get(endpoint: string, token?: string): Promise { return this.makeRequest(endpoint, { method: 'GET' }, token) } static async post( endpoint: string, data?: any, token?: string ): Promise { return this.makeRequest( endpoint, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }, token ) } static async postFormData( endpoint: string, formData: FormData, token?: string ): Promise { const url = `${API_BASE_URL}${endpoint}` const headers: HeadersInit = {} if (token) { headers['Authorization'] = `Bearer ${token}` } return fetch(url, { method: 'POST', headers, body: formData, }) } static async put( endpoint: string, data?: any, token?: string ): Promise { return this.makeRequest( endpoint, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }, token ) } static async patch( endpoint: string, data?: any, token?: string ): Promise { return this.makeRequest( endpoint, { method: 'PATCH', body: data ? JSON.stringify(data) : undefined, }, token ) } static async delete(endpoint: string, token?: string, data?: any): Promise { return this.makeRequest( endpoint, { method: 'DELETE', body: data ? JSON.stringify(data) : undefined, }, token ) } } // Specific API Functions for QuickActions export class QuickActionsAPI { static async getUserStatus(token: string) { const response = await ApiClient.get(API_ENDPOINTS.USER_STATUS, token) if (!response.ok) { throw new Error('Failed to fetch user status') } return response.json() } static async sendVerificationEmail(token: string) { const response = await ApiClient.post(API_ENDPOINTS.SEND_VERIFICATION_EMAIL, {}, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to send verification email') } return response.json() } static async verifyEmailCode(token: string, code: string) { const response = await ApiClient.post( API_ENDPOINTS.VERIFY_EMAIL_CODE, { code }, token ) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Invalid verification code') } return response.json() } static async uploadDocuments(token: string, userType: string, files: { front: File, back?: File }) { const formData = new FormData() formData.append('front', files.front) if (files.back) { formData.append('back', files.back) } const endpoint = userType === 'company' ? API_ENDPOINTS.UPLOAD_COMPANY_ID : API_ENDPOINTS.UPLOAD_PERSONAL_ID const response = await ApiClient.postFormData(endpoint, formData, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to upload documents') } return response.json() } static async completeProfile(token: string, userType: string, profileData: any) { const endpoint = userType === 'company' ? API_ENDPOINTS.COMPLETE_COMPANY_PROFILE : API_ENDPOINTS.COMPLETE_PERSONAL_PROFILE const response = await ApiClient.post(endpoint, profileData, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to complete profile') } return response.json() } static async uploadContract(token: string, userType: string, contractFile: File) { const formData = new FormData() formData.append('contract', contractFile) const endpoint = userType === 'company' ? API_ENDPOINTS.UPLOAD_COMPANY_CONTRACT : API_ENDPOINTS.UPLOAD_PERSONAL_CONTRACT const response = await ApiClient.postFormData(endpoint, formData, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to upload contract') } return response.json() } } // Error Types export interface ApiError { message: string status?: number code?: string } // Admin API Functions export class AdminAPI { static async getUserStats(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_STATS, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch user stats' })) throw new Error(error.message || 'Failed to fetch user stats') } return response.json() } static async getUserList(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_USER_LIST, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch user list' })) throw new Error(error.message || 'Failed to fetch user list') } return response.json() } static async getVerificationPendingUsers(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_VERIFICATION_PENDING, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch pending users' })) throw new Error(error.message || 'Failed to fetch pending users') } return response.json() } static async getUnverifiedUsers(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_UNVERIFIED_USERS, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch unverified users' })) throw new Error(error.message || 'Failed to fetch unverified users') } return response.json() } static async getDetailedUserInfo(token: string, userId: string) { const endpoint = API_ENDPOINTS.ADMIN_USER_DETAILED.replace(':id', userId) const response = await ApiClient.get(endpoint, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch detailed user info' })) throw new Error(error.message || 'Failed to fetch detailed user info') } return response.json() } static async verifyUser(token: string, userId: string, permissions: string[] = []) { const endpoint = API_ENDPOINTS.ADMIN_VERIFY_USER.replace(':id', userId) const response = await ApiClient.post(endpoint, { permissions }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to verify user') } return response.json() } static async archiveUser(token: string, userId: string) { const endpoint = API_ENDPOINTS.ADMIN_ARCHIVE_USER.replace(':id', userId) const response = await ApiClient.patch(endpoint, { status: 'inactive' }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to archive user') } return response.json() } static async unarchiveUser(token: string, userId: string) { const endpoint = API_ENDPOINTS.ADMIN_UNARCHIVE_USER.replace(':id', userId) const response = await ApiClient.patch(endpoint, { status: 'active' }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to unarchive user') } return response.json() } static async updateUserVerification(token: string, userId: string, isVerified: number) { const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_VERIFICATION.replace(':id', userId) const response = await ApiClient.patch(endpoint, { is_admin_verified: isVerified }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to update user verification') } return response.json() } static async updateUserProfile(token: string, userId: string, profileData: any, userType: string) { const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_PROFILE.replace(':id', userId) const response = await ApiClient.patch(endpoint, { profileData, userType }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to update user profile') } return response.json() } static async getPoolMembers(token: string, poolId: string | number) { const endpoint = API_ENDPOINTS.ADMIN_POOL_MEMBERS.replace(':id', String(poolId)) const response = await ApiClient.get(endpoint, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch pool members' })) throw new Error(error.message || 'Failed to fetch pool members') } return response.json() } static async addPoolMembers(token: string, poolId: string | number, userIds: Array) { const endpoint = API_ENDPOINTS.ADMIN_POOL_MEMBERS.replace(':id', String(poolId)) const response = await ApiClient.post(endpoint, { userIds }, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to add pool members' })) throw new Error(error.message || 'Failed to add pool members') } return response.json() } static async removePoolMembers(token: string, poolId: string | number, userIds: Array) { const endpoint = API_ENDPOINTS.ADMIN_POOL_MEMBERS.replace(':id', String(poolId)) const response = await ApiClient.delete(endpoint, token, { userIds }) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to remove pool members' })) throw new Error(error.message || 'Failed to remove pool members') } return response.json() } static async updateUserStatus(token: string, userId: string, status: string) { const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_STATUS.replace(':id', userId) const response = await ApiClient.patch(endpoint, { status }, token) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to update user status') } return response.json() } // Coffee products (admin) static async listCoffee(token: string) { const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token) if (!response.ok) throw new Error('Failed to list products') return response.json() } static async createCoffee(token: string, data: { title: string; description: string; quantity: number; price: number; currency?: string; tax_rate?: number; is_featured?: boolean; billing_interval?: 'day'|'week'|'month'|'year'; interval_count?: number; sku?: string; slug?: string; state?: boolean; pictureFile?: File }) { if (data.pictureFile) { const fd = new FormData() fd.append('title', data.title) fd.append('description', data.description) fd.append('quantity', String(data.quantity)) fd.append('price', String(data.price)) if (data.currency) fd.append('currency', data.currency) if (data.tax_rate !== undefined) fd.append('tax_rate', String(data.tax_rate)) if (data.is_featured !== undefined) fd.append('is_featured', String(!!data.is_featured)) if (data.billing_interval) fd.append('billing_interval', data.billing_interval) if (data.interval_count !== undefined) fd.append('interval_count', String(data.interval_count)) if (data.sku) fd.append('sku', data.sku) if (data.slug) fd.append('slug', data.slug) if (data.state !== undefined) fd.append('state', String(!!data.state)) fd.append('picture', data.pictureFile) const resp = await ApiClient.postFormData(API_ENDPOINTS.ADMIN_COFFEE_CREATE, fd, token) if (!resp.ok) throw new Error('Failed to create product') return resp.json() } else { const resp = await ApiClient.post(API_ENDPOINTS.ADMIN_COFFEE_CREATE, { title: data.title, description: data.description, quantity: data.quantity, price: data.price, currency: data.currency, tax_rate: data.tax_rate, is_featured: data.is_featured, billing_interval: data.billing_interval, interval_count: data.interval_count, sku: data.sku, slug: data.slug, state: data.state, }, token) if (!resp.ok) throw new Error('Failed to create product') return resp.json() } } static async updateCoffee(token: string, id: number, data: { title?: string; description?: string; quantity?: number; price?: number; currency?: string; tax_rate?: number; is_featured?: boolean; billing_interval?: 'day'|'week'|'month'|'year'; interval_count?: number; sku?: string; slug?: string; state?: boolean; pictureFile?: File }) { if (data.pictureFile) { const fd = new FormData() if (data.title !== undefined) fd.append('title', data.title) if (data.description !== undefined) fd.append('description', data.description) if (data.quantity !== undefined) fd.append('quantity', String(data.quantity)) if (data.price !== undefined) fd.append('price', String(data.price)) if (data.currency !== undefined) fd.append('currency', data.currency) if (data.tax_rate !== undefined) fd.append('tax_rate', String(data.tax_rate)) if (data.is_featured !== undefined) fd.append('is_featured', String(!!data.is_featured)) if (data.billing_interval !== undefined) fd.append('billing_interval', data.billing_interval) if (data.interval_count !== undefined) fd.append('interval_count', String(data.interval_count)) if (data.sku !== undefined) fd.append('sku', data.sku) if (data.slug !== undefined) fd.append('slug', data.slug) if (data.state !== undefined) fd.append('state', String(!!data.state)) fd.append('picture', data.pictureFile) const endpoint = API_ENDPOINTS.ADMIN_COFFEE_UPDATE.replace(':id', String(id)) const resp = await ApiClient.postFormData(endpoint, fd, token) if (!resp.ok) throw new Error('Failed to update product') return resp.json() } else { const endpoint = API_ENDPOINTS.ADMIN_COFFEE_UPDATE.replace(':id', String(id)) const resp = await ApiClient.put(endpoint, data, token) if (!resp.ok) throw new Error('Failed to update product') return resp.json() } } static async setCoffeeState(token: string, id: number, state: boolean) { const endpoint = API_ENDPOINTS.ADMIN_COFFEE_SET_STATE.replace(':id', String(id)) const resp = await ApiClient.patch(endpoint, { state }, token) if (!resp.ok) throw new Error('Failed to set state') return resp.json() } static async deleteCoffee(token: string, id: number) { const endpoint = API_ENDPOINTS.ADMIN_COFFEE_DELETE.replace(':id', String(id)) const resp = await ApiClient.delete(endpoint, token) if (!resp.ok) throw new Error('Failed to delete product') return true } static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company', contractType?: 'contract' | 'gdpr', documentId?: number, objectKey?: string) { let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_PREVIEW.replace(':id', userId) const qs = new URLSearchParams() if (userType) qs.set('userType', userType) if (contractType) qs.set('contract_type', contractType) if (documentId) qs.set('documentId', String(documentId)) if (objectKey) qs.set('objectKey', objectKey) const qsStr = qs.toString() if (qsStr) endpoint += `?${qsStr}` const response = await ApiClient.get(endpoint, token) const warningHeader = response.headers.get('x-contract-preview-warning') if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to fetch contract preview' })) const err: any = new Error(error.message || 'Failed to fetch contract preview') if (warningHeader) err.warning = warningHeader if (error?.warning) err.warning = error.warning throw err } // Return HTML string const html = await response.text() return { html, warning: warningHeader } } static async listContractDocuments(token: string, userId: string, userType?: 'personal' | 'company') { let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_DOCS.replace(':id', userId) const qs = new URLSearchParams() if (userType) qs.set('userType', userType) const qsStr = qs.toString() if (qsStr) endpoint += `?${qsStr}` const response = await ApiClient.get(endpoint, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to load contract documents' })) throw new Error(error.message || 'Failed to load contract documents') } return response.json() } static async listContractFiles(token: string, userId: string, userType?: 'personal' | 'company') { let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_FILES.replace(':id', userId) const qs = new URLSearchParams() if (userType) qs.set('userType', userType) const qsStr = qs.toString() if (qsStr) endpoint += `?${qsStr}` const response = await ApiClient.get(endpoint, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to load contract files' })) throw new Error(error.message || 'Failed to load contract files') } return response.json() } static async moveContractDocument(token: string, userId: string, documentId: number | undefined, targetType: 'contract' | 'gdpr', objectKey?: string) { const endpoint = API_ENDPOINTS.ADMIN_CONTRACT_MOVE.replace(':id', userId) const response = await ApiClient.post(endpoint, { documentId, targetType, objectKey }, token) if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Failed to move contract document' })) throw new Error(error.message || 'Failed to move contract document') } return response.json() } } // Response Types export interface UserStatus { emailVerified: boolean documentsUploaded: boolean profileCompleted: boolean contractSigned: boolean } export interface AdminUserStats { totalUsers: number adminUsers: number verificationPending: number activeUsers: number personalUsers: number companyUsers: number } export interface PendingUser { id: number email: string user_type: 'personal' | 'company' role: 'user' | 'admin' created_at: string last_login_at: string | null status: string is_admin_verified: number email_verified?: number profile_completed?: number documents_uploaded?: number contract_signed?: number first_name?: string last_name?: string company_name?: string } export interface DetailedUserInfo { user: { id: number email: string user_type: 'personal' | 'company' role: 'user' | 'admin' | 'super_admin' created_at: string last_login_at: string | null } personalProfile?: { user_id: number first_name: string last_name: string date_of_birth?: string phone?: string address?: string city?: string zip_code?: string country?: string } companyProfile?: { user_id: number company_name: string tax_id?: string registration_number?: string phone?: string address?: string city?: string zip_code?: string country?: string } userStatus?: { user_id: number status: string email_verified: number profile_completed: number documents_uploaded: number contract_signed: number is_admin_verified: number admin_verified_at?: string } permissions: Array<{ id: number name: string description: string is_active: boolean }> documents: Array<{ id: number user_id: number document_type: string file_name: string file_size: number uploaded_at: string }> contracts: Array<{ id: number user_id: number document_type: string file_name: string file_size: number uploaded_at: string }> idDocuments: Array<{ id: number user_id: number document_type: string front_object_storage_id?: string back_object_storage_id?: string frontUrl?: string backUrl?: string uploaded_at: string }> storageStatus?: { idDocumentsPresent?: boolean contractPresent?: boolean } } export interface ApiResponse { success: boolean message?: string data?: T }