feat: initial admin panel
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { apiClient } from './client';
|
||||
import { LoginCredentials, LoginResponse } from '@/types/auth';
|
||||
|
||||
export const authApi = {
|
||||
login: async (credentials: LoginCredentials): Promise<LoginResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append('auth', credentials.auth);
|
||||
formData.append('password', credentials.password);
|
||||
formData.append('package_name', credentials.package_name);
|
||||
|
||||
return apiClient.post<LoginResponse>('/auth/login', formData);
|
||||
},
|
||||
|
||||
logout: async (token: string): Promise<void> => {
|
||||
return apiClient.post('/auth/logout', {}, token);
|
||||
},
|
||||
|
||||
getProfile: async (token: string): Promise<any> => {
|
||||
return apiClient.get('/auth/profile', token);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.approagency.ir/api';
|
||||
|
||||
interface ApiOptions extends RequestInit {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: ApiOptions = {}
|
||||
): Promise<T> {
|
||||
const { token, ...fetchOptions } = options;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
|
||||
// Don't set Content-Type for FormData (browser will set it automatically with boundary)
|
||||
if (!(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.message || 'API request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
||||
const body = data instanceof FormData ? data : JSON.stringify(data);
|
||||
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string, token?: string): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'GET',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
async put<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
||||
const body = data instanceof FormData ? data : JSON.stringify(data);
|
||||
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string, data?: any, token?: string): Promise<T> {
|
||||
const options: ApiOptions = {
|
||||
method: 'DELETE',
|
||||
token,
|
||||
};
|
||||
|
||||
// Handle DELETE with body if data is provided
|
||||
if (data) {
|
||||
options.body = data instanceof FormData ? data : JSON.stringify(data);
|
||||
|
||||
// Don't set Content-Type for FormData
|
||||
if (!(data instanceof FormData)) {
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return this.request<T>(endpoint, options);
|
||||
}
|
||||
|
||||
// Convenience method for DELETE with JSON body
|
||||
async deleteWithBody<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
||||
return this.delete<T>(endpoint, data, token);
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data: any, token?: string): Promise<T> {
|
||||
const body = data instanceof FormData ? data : JSON.stringify(data);
|
||||
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
@@ -0,0 +1,78 @@
|
||||
import { apiClient } from './client';
|
||||
import { PackageName, CreatePackageData, UpdatePackageData, ApiResponse } from '@/types/package';
|
||||
|
||||
export const packagesApi = {
|
||||
// Get all package names
|
||||
getAllPackages: async (token: string): Promise<PackageName[]> => {
|
||||
return apiClient.get<PackageName[]>('/package-names', token);
|
||||
},
|
||||
|
||||
// Get single package by name
|
||||
getPackageByName: async (name: string, token: string): Promise<PackageName> => {
|
||||
return apiClient.get<PackageName>(`/package-names/${name}`, token);
|
||||
},
|
||||
|
||||
// Create new package
|
||||
createPackage: async (data: CreatePackageData, token: string): Promise<ApiResponse<PackageName>> => {
|
||||
const formData = new FormData();
|
||||
|
||||
// Append all fields to FormData
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (key === 'avatar' && value instanceof File) {
|
||||
formData.append('avatar', value);
|
||||
} else if (key === 'firebase_json' && value instanceof File) {
|
||||
formData.append('firebase_json', value);
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return apiClient.post<ApiResponse<PackageName>>('/package-names', formData, token);
|
||||
},
|
||||
|
||||
// Update package
|
||||
updatePackage: async (name: string, data: UpdatePackageData, token: string): Promise<ApiResponse<PackageName>> => {
|
||||
const formData = new FormData();
|
||||
|
||||
// Add method spoofing for Laravel
|
||||
formData.append('_method', 'PUT');
|
||||
|
||||
// Append all fields to FormData
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (key === 'avatar' && value instanceof File) {
|
||||
formData.append('avatar', value);
|
||||
} else if (key === 'firebase_json' && value instanceof File) {
|
||||
formData.append('firebase_json', value);
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return apiClient.post<ApiResponse<PackageName>>(`/package-names/${name}`, formData, token);
|
||||
},
|
||||
|
||||
// Delete package
|
||||
deletePackage: async (name: string, token: string): Promise<ApiResponse<any>> => {
|
||||
return apiClient.delete<ApiResponse<any>>(`/package-names/${name}`, undefined, token);
|
||||
},
|
||||
|
||||
// Upload avatar separately if needed
|
||||
uploadAvatar: async (name: string, avatarFile: File, token: string): Promise<ApiResponse<any>> => {
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', avatarFile);
|
||||
|
||||
return apiClient.post<ApiResponse<any>>(`/package-names/${name}/avatar`, formData, token);
|
||||
},
|
||||
|
||||
// Upload firebase config separately if needed
|
||||
uploadFirebaseConfig: async (name: string, firebaseFile: File, token: string): Promise<ApiResponse<any>> => {
|
||||
const formData = new FormData();
|
||||
formData.append('firebase_json', firebaseFile);
|
||||
|
||||
return apiClient.post<ApiResponse<any>>(`/package-names/${name}/firebase`, formData, token);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { apiClient } from './client';
|
||||
import { Product, CreateProductData, UpdateProductData, ApiResponse } from '@/types/product';
|
||||
|
||||
export const productsApi = {
|
||||
// Get all products for a package
|
||||
getProducts: async (packageName: string, token: string): Promise<Product[]> => {
|
||||
return apiClient.get<Product[]>(`/package-names/${packageName}/products`, token);
|
||||
},
|
||||
|
||||
// Get single product
|
||||
getProduct: async (packageName: string, productId: number, token: string): Promise<Product> => {
|
||||
return apiClient.get<Product>(`/package-names/${packageName}/products/${productId}`, token);
|
||||
},
|
||||
|
||||
// Create new product
|
||||
createProduct: async (packageName: string, data: CreateProductData, token: string): Promise<ApiResponse<Product>> => {
|
||||
const formData = new FormData();
|
||||
|
||||
// Append all fields to FormData
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (key === 'descriptions' && Array.isArray(value)) {
|
||||
// Handle descriptions array
|
||||
value.forEach((desc, index) => {
|
||||
formData.append(`descriptions[${index}]`, desc);
|
||||
});
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return apiClient.post<ApiResponse<Product>>(`/package-names/${packageName}/products`, formData, token);
|
||||
},
|
||||
|
||||
// Update product (using PATCH)
|
||||
updateProduct: async (packageName: string, productId: number, data: UpdateProductData, token: string): Promise<ApiResponse<Product>> => {
|
||||
return apiClient.patch<ApiResponse<Product>>(
|
||||
`/package-names/${packageName}/products/${productId}`,
|
||||
data,
|
||||
token
|
||||
);
|
||||
},
|
||||
|
||||
// Delete product
|
||||
deleteProduct: async (packageName: string, productId: number, token: string): Promise<ApiResponse<any>> => {
|
||||
return apiClient.delete<ApiResponse<any>>(`/package-names/${packageName}/products/${productId}`, undefined, token);
|
||||
},
|
||||
|
||||
// Alternative update method using POST with _method spoofing (if PATCH is not supported)
|
||||
updateProductWithSpoof: async (packageName: string, productId: number, data: UpdateProductData, token: string): Promise<ApiResponse<Product>> => {
|
||||
const formData = new FormData();
|
||||
formData.append('_method', 'PATCH');
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
if (key === 'descriptions' && Array.isArray(value)) {
|
||||
value.forEach((desc, index) => {
|
||||
formData.append(`descriptions[${index}]`, desc);
|
||||
});
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return apiClient.post<ApiResponse<Product>>(`/package-names/${packageName}/products/${productId}`, formData, token);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { apiClient } from './client';
|
||||
import { User, Product, ApiResponse } from '@/types/user';
|
||||
|
||||
export const usersApi = {
|
||||
// Get user status by email or mobile - package name is now a parameter
|
||||
getUserStatus: async (identifier: string, packageName: string, token: string): Promise<User> => {
|
||||
// Check if identifier is email or mobile
|
||||
const isEmail = identifier.includes('@');
|
||||
const endpoint = isEmail
|
||||
? `/admin/users/${identifier}/status?package_name=${packageName}`
|
||||
: `/admin/users/${identifier}/status?package_name=${packageName}`;
|
||||
|
||||
return apiClient.get<User>(endpoint, token);
|
||||
},
|
||||
|
||||
// Subscribe user to a product
|
||||
subscribeUser: async (identifier: string, productId: number, token: string): Promise<ApiResponse<any>> => {
|
||||
const data = {
|
||||
product_id: productId
|
||||
};
|
||||
|
||||
return apiClient.put<ApiResponse<any>>(
|
||||
`/admin/users/${identifier}/status`,
|
||||
data,
|
||||
token
|
||||
);
|
||||
},
|
||||
|
||||
// Unsubscribe user from a product
|
||||
unsubscribeUser: async (identifier: string, productId: number, token: string): Promise<ApiResponse<any>> => {
|
||||
const data = {
|
||||
product_id: productId
|
||||
};
|
||||
|
||||
return apiClient.delete<ApiResponse<any>>(
|
||||
`/admin/users/${identifier}/status`,
|
||||
data,
|
||||
token
|
||||
);
|
||||
},
|
||||
|
||||
// Get user transactions
|
||||
getUserTransactions: async (userId: number, packageName: string, token: string): Promise<any> => {
|
||||
return apiClient.get(`/admin/users/${userId}/transactions?package_name=${packageName}`, token);
|
||||
},
|
||||
|
||||
// Get products for a specific package
|
||||
getProductsByPackage: async (packageName: string, token: string): Promise<Product[]> => {
|
||||
return apiClient.get<Product[]>(`/package-names/${packageName}/products`, token);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { User } from "@/types/user";
|
||||
|
||||
export const formatDate = (dateString: string | null): string => {
|
||||
if (!dateString) return 'نامشخص';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat('fa-IR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
} catch {
|
||||
return 'نامشخص';
|
||||
}
|
||||
};
|
||||
|
||||
export const formatPrice = (price: number | null): string => {
|
||||
if (price === null || price === undefined) return '0';
|
||||
return new Intl.NumberFormat('fa-IR').format(price);
|
||||
};
|
||||
|
||||
export const getProductTypeLabel = (type: number | null): string => {
|
||||
const types: Record<number, string> = {
|
||||
1: 'ماهیانه',
|
||||
2: 'سه ماهه',
|
||||
3: 'شش ماهه',
|
||||
4: 'سالیانه',
|
||||
};
|
||||
return type && types[type] ? types[type] : 'نامشخص';
|
||||
};
|
||||
|
||||
export const getFullName = (user: User): string => {
|
||||
if (user.full_name) return user.full_name;
|
||||
|
||||
const firstName = user.first_name || '';
|
||||
const lastName = user.last_name || '';
|
||||
|
||||
if (firstName || lastName) {
|
||||
return `${firstName} ${lastName}`.trim();
|
||||
}
|
||||
|
||||
return 'نامشخص';
|
||||
};
|
||||
|
||||
export const getEmail = (email: string | null): string => {
|
||||
return email || 'ایمیل ثبت نشده';
|
||||
};
|
||||
|
||||
export const getMobile = (mobile: string | null): string => {
|
||||
return mobile || 'شماره موبایل ثبت نشده';
|
||||
};
|
||||
Reference in New Issue
Block a user