feat: accounting added
This commit is contained in:
+48
-13
@@ -1,22 +1,57 @@
|
||||
import { apiClient } from './client';
|
||||
import { Purchase, Paginated, PurchaseFilters } from '@/types/purchase';
|
||||
|
||||
const buildPurchaseQuery = (filters: PurchaseFilters): string => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.email) params.append('email', filters.email);
|
||||
if (filters.mobile) params.append('mobile', filters.mobile);
|
||||
if (filters.package_name) params.append('package_name', filters.package_name);
|
||||
if (filters.gateway) params.append('gateway', filters.gateway);
|
||||
if (filters.status !== undefined && filters.status !== null) {
|
||||
params.append('status', String(filters.status));
|
||||
}
|
||||
if (filters.product_id !== undefined && filters.product_id !== null) {
|
||||
params.append('product_id', String(filters.product_id));
|
||||
}
|
||||
if (filters.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const purchasesApi = {
|
||||
// List all subscription purchases with optional filters
|
||||
getPurchases: async (filters: PurchaseFilters, token: string): Promise<Paginated<Purchase>> => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.email) params.append('email', filters.email);
|
||||
if (filters.mobile) params.append('mobile', filters.mobile);
|
||||
if (filters.package_name) params.append('package_name', filters.package_name);
|
||||
if (filters.gateway) params.append('gateway', filters.gateway);
|
||||
if (filters.status !== undefined && filters.status !== null) {
|
||||
params.append('status', String(filters.status));
|
||||
}
|
||||
if (filters.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters.page) params.append('page', String(filters.page));
|
||||
|
||||
const qs = params.toString();
|
||||
const qs = buildPurchaseQuery(filters);
|
||||
return apiClient.get<Paginated<Purchase>>(`/admin/purchases${qs ? `?${qs}` : ''}`, token);
|
||||
},
|
||||
|
||||
// Fetch ALL purchases matching the filters (used by the accounting section).
|
||||
// Learns the total from page 1, then fetches the remaining pages concurrently.
|
||||
getAllPurchases: async (filters: PurchaseFilters, token: string): Promise<Purchase[]> => {
|
||||
const perPage = filters.per_page || 500;
|
||||
const first = await purchasesApi.getPurchases({ ...filters, per_page: perPage, page: 1 }, token);
|
||||
|
||||
const all = new Map<number, Purchase>();
|
||||
first.data.forEach((p) => all.set(p.id, p));
|
||||
|
||||
const lastPage = first.last_page ?? 1;
|
||||
if (lastPage > 1) {
|
||||
const pages: number[] = [];
|
||||
for (let page = 2; page <= lastPage; page++) pages.push(page);
|
||||
|
||||
// Fetch in small concurrent batches to avoid hammering the API
|
||||
const batchSize = 4;
|
||||
for (let i = 0; i < pages.length; i += batchSize) {
|
||||
const batch = pages.slice(i, i + batchSize);
|
||||
const results = await Promise.all(
|
||||
batch.map((page) => purchasesApi.getPurchases({ ...filters, per_page: perPage, page }, token))
|
||||
);
|
||||
results.forEach((res) => res.data.forEach((p) => all.set(p.id, p)));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(all.values());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import { User } from "@/types/user";
|
||||
import { Purchase } from "@/types/purchase";
|
||||
|
||||
// Single source of truth for product type labels lives in types/product.ts
|
||||
// (kept in sync with the backend Product::TYPES). Re-exported here so existing
|
||||
// `@/lib/utils` imports keep working without a divergent (previously wrong) map.
|
||||
export { getProductTypeLabel } from "@/types/product";
|
||||
|
||||
// قیمت نهایی خرید — shared between purchases table and accounting page.
|
||||
// اگر محصول تخفیف داشته باشد قیمت تخفیفخورده، در غیر این صورت قیمت اصلی، و در نهایت مبلغ تراکنش.
|
||||
export const getPurchaseFinalPrice = (purchase: Purchase): number => {
|
||||
const product = purchase.product;
|
||||
|
||||
// اگر محصول تخفیف دارد، قیمت نهایی همان قیمت تخفیفخورده است
|
||||
const discountedPrice = product?.discounted_price;
|
||||
if (discountedPrice) {
|
||||
const parsed = parseInt(discountedPrice.replace(/[^\d]/g, ''), 10);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
|
||||
// بدون تخفیف → قیمت اصلی محصول
|
||||
if (product?.price) return product.price;
|
||||
|
||||
// برگشت به مبلغ ثبتشدهٔ تراکنش وقتی محصول در دسترس نیست
|
||||
return purchase.amount || 0;
|
||||
};
|
||||
|
||||
// Parse a percentage input (e.g. "۱۵%" / "15") and clamp to 0–100.
|
||||
export const parseStoreRate = (value: string | number): number => {
|
||||
if (value === null || value === undefined || value === '') return 0;
|
||||
const str = String(value).replace(/[^\d]/g, '');
|
||||
if (!str) return 0;
|
||||
const n = parseInt(str, 10);
|
||||
if (Number.isNaN(n)) return 0;
|
||||
return Math.min(100, Math.max(0, n));
|
||||
};
|
||||
|
||||
export const formatDate = (dateString: string | null): string => {
|
||||
if (!dateString) return 'نامشخص';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user