profit-planet-frontend/src/app/utils/api.ts

584 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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',
// 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',
}
// API Helper Functions
export class ApiClient {
static async makeRequest(
endpoint: string,
options: RequestInit = {},
token?: string
): Promise<Response> {
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<Response> {
return this.makeRequest(endpoint, { method: 'GET' }, token)
}
static async post(
endpoint: string,
data?: any,
token?: string
): Promise<Response> {
return this.makeRequest(
endpoint,
{
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
},
token
)
}
static async postFormData(
endpoint: string,
formData: FormData,
token?: string
): Promise<Response> {
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<Response> {
return this.makeRequest(
endpoint,
{
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
},
token
)
}
static async patch(
endpoint: string,
data?: any,
token?: string
): Promise<Response> {
return this.makeRequest(
endpoint,
{
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
},
token
)
}
static async delete(endpoint: string, token?: string): Promise<Response> {
return this.makeRequest(endpoint, { method: 'DELETE' }, 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 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') {
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)
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 }
}
}
// 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<T = any> {
success: boolean
message?: string
data?: T
}