feat: initial admin panel

This commit is contained in:
2026-02-19 22:13:11 +03:30
parent 70efff0dfd
commit 9aff48019d
50 changed files with 9604 additions and 1 deletions
+21
View File
@@ -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);
},
};
+104
View File
@@ -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();
+78
View File
@@ -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);
}
};
+69
View File
@@ -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);
}
};
+51
View File
@@ -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);
}
};