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);
}
};
+52
View File
@@ -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 || 'شماره موبایل ثبت نشده';
};