Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3071b8692 | ||
|
|
ba7147ff59 |
+125
-33
@@ -10,7 +10,7 @@ import PersianDatePicker from '@/components/admin/PersianDatePicker';
|
|||||||
import { Purchase, PAYMENT_GATEWAYS } from '@/types/purchase';
|
import { Purchase, PAYMENT_GATEWAYS } from '@/types/purchase';
|
||||||
import { PackageName } from '@/types/package';
|
import { PackageName } from '@/types/package';
|
||||||
import { Product } from '@/types/product';
|
import { Product } from '@/types/product';
|
||||||
import { formatPrice, getPurchaseFinalPrice, parseStoreRate } from '@/lib/utils';
|
import { formatPrice, getPurchaseFinalPrice, parseStoreRate, parseFee } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
@@ -29,7 +29,7 @@ interface StoreSummary {
|
|||||||
count: number;
|
count: number;
|
||||||
gross: number;
|
gross: number;
|
||||||
rate: number;
|
rate: number;
|
||||||
commission: number;
|
fee: number;
|
||||||
net: number;
|
net: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,24 +55,42 @@ interface AccountingResult {
|
|||||||
const computeAccounting = (
|
const computeAccounting = (
|
||||||
purchases: Purchase[],
|
purchases: Purchase[],
|
||||||
myketRate: number,
|
myketRate: number,
|
||||||
cafeRate: number
|
cafeRate: number,
|
||||||
|
transactionFee: number,
|
||||||
): AccountingResult => {
|
): AccountingResult => {
|
||||||
const rows = new Map<number, ProductRow>();
|
const rows = new Map<number, ProductRow>();
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
let totalGross = 0;
|
let totalGross = 0;
|
||||||
let totalCommission = 0;
|
let totalCommission = 0;
|
||||||
|
|
||||||
const makeStore = (rate: number): StoreSummary => ({ count: 0, gross: 0, rate, commission: 0, net: 0 });
|
// درصد سهم توسعهدهنده بهصورت کسر (0.85) و کارمزد تراکنش به ریال
|
||||||
|
const makeStore = (rate: number): StoreSummary => ({ count: 0, gross: 0, rate, fee: 0, net: 0 });
|
||||||
const myket: StoreSummary = makeStore(myketRate);
|
const myket: StoreSummary = makeStore(myketRate);
|
||||||
const cafe: StoreSummary = makeStore(cafeRate);
|
const cafe: StoreSummary = makeStore(cafeRate);
|
||||||
const other: StoreSummary = makeStore(0);
|
const other: StoreSummary = makeStore(0);
|
||||||
|
|
||||||
const collect = (store: StoreSummary, price: number, rate: number) => {
|
// سهم توسعهدهنده برای مایکت/کافهبازار:
|
||||||
const commission = (price * rate) / 100;
|
// (P / 1.1 - T) × R
|
||||||
|
// P = قیمت فروش (با احتساب ۱۰٪ مالیات بر ارزش افزوده)
|
||||||
|
// /1.1 = حذف ارزش افزوده، T = کارمزد تراکنش به ریال، R = درصد سهم
|
||||||
|
const developerShare = (price: number, rate: number, feePerTransaction: number) => {
|
||||||
|
const beforeVat = price / 1.1;
|
||||||
|
const afterVatAndFee = beforeVat - feePerTransaction;
|
||||||
|
if (afterVatAndFee <= 0) return 0;
|
||||||
|
return afterVatAndFee * (rate / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collect = (store: StoreSummary, price: number, rate: number, feePerTransaction: number) => {
|
||||||
|
let net: number;
|
||||||
|
if (rate > 0) {
|
||||||
|
net = developerShare(price, rate, feePerTransaction);
|
||||||
|
} else {
|
||||||
|
net = price; // سایر درگاهها: بدون کارمزد فروشگاه
|
||||||
|
}
|
||||||
store.count += 1;
|
store.count += 1;
|
||||||
store.gross += price;
|
store.gross += price;
|
||||||
store.commission += commission;
|
store.fee += price - net;
|
||||||
store.net += price - commission;
|
store.net += net;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const purchase of purchases) {
|
for (const purchase of purchases) {
|
||||||
@@ -81,22 +99,26 @@ const computeAccounting = (
|
|||||||
|
|
||||||
let store: StoreSummary;
|
let store: StoreSummary;
|
||||||
let rate: number;
|
let rate: number;
|
||||||
|
let feePerTransaction: number;
|
||||||
if (gateway === PAYMENT_GATEWAYS.cafe.code) {
|
if (gateway === PAYMENT_GATEWAYS.cafe.code) {
|
||||||
store = cafe;
|
store = cafe;
|
||||||
rate = cafeRate;
|
rate = cafeRate;
|
||||||
|
feePerTransaction = transactionFee;
|
||||||
} else if (gateway === PAYMENT_GATEWAYS.myket.code) {
|
} else if (gateway === PAYMENT_GATEWAYS.myket.code) {
|
||||||
store = myket;
|
store = myket;
|
||||||
rate = myketRate;
|
rate = myketRate;
|
||||||
|
feePerTransaction = transactionFee;
|
||||||
} else {
|
} else {
|
||||||
store = other;
|
store = other;
|
||||||
rate = 0;
|
rate = 0;
|
||||||
|
feePerTransaction = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
collect(store, price, rate);
|
collect(store, price, rate, feePerTransaction);
|
||||||
|
|
||||||
totalCount += 1;
|
totalCount += 1;
|
||||||
totalGross += price;
|
totalGross += price;
|
||||||
totalCommission += (price * rate) / 100;
|
totalCommission += price - developerShare(price, rate, feePerTransaction);
|
||||||
|
|
||||||
const productId = purchase.product_id ?? purchase.product?.id ?? -1;
|
const productId = purchase.product_id ?? purchase.product?.id ?? -1;
|
||||||
let row = rows.get(productId);
|
let row = rows.get(productId);
|
||||||
@@ -117,8 +139,8 @@ const computeAccounting = (
|
|||||||
}
|
}
|
||||||
row.count += 1;
|
row.count += 1;
|
||||||
row.gross += price;
|
row.gross += price;
|
||||||
row.commission += (price * rate) / 100;
|
row.commission += price - developerShare(price, rate, feePerTransaction);
|
||||||
row.net += price - (price * rate) / 100;
|
row.net += developerShare(price, rate, feePerTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -148,12 +170,18 @@ export default function AccountingPage() {
|
|||||||
const [dateFrom, setDateFrom] = useState<string>('');
|
const [dateFrom, setDateFrom] = useState<string>('');
|
||||||
const [dateTo, setDateTo] = useState<string>('');
|
const [dateTo, setDateTo] = useState<string>('');
|
||||||
|
|
||||||
|
// Time range
|
||||||
|
const [timeFrom, setTimeFrom] = useState<string>('');
|
||||||
|
const [timeTo, setTimeTo] = useState<string>('');
|
||||||
|
|
||||||
// Status filter — both paid statuses selected by default
|
// Status filter — both paid statuses selected by default
|
||||||
const [statuses, setStatuses] = useState<number[]>([...PAID_STATUSES]);
|
const [statuses, setStatuses] = useState<number[]>([...PAID_STATUSES]);
|
||||||
|
|
||||||
// Store commission rates (editable percentages)
|
// درصد سهم توسعهدهنده (R در فرمول) — مایکت و کافهبازار بهطور پیشفرض ۸۵٪
|
||||||
const [myketRate, setMyketRate] = useState<string>('15');
|
const [myketRate, setMyketRate] = useState<string>('85');
|
||||||
const [cafeRate, setCafeRate] = useState<string>('15');
|
const [cafeRate, setCafeRate] = useState<string>('85');
|
||||||
|
// کارمزد تراکنش به ریال (T در فرمول سهم توسعهدهنده)
|
||||||
|
const [transactionFee, setTransactionFee] = useState<string>('1200');
|
||||||
|
|
||||||
const [allPurchases, setAllPurchases] = useState<Purchase[] | null>(null);
|
const [allPurchases, setAllPurchases] = useState<Purchase[] | null>(null);
|
||||||
const [result, setResult] = useState<AccountingResult | null>(null);
|
const [result, setResult] = useState<AccountingResult | null>(null);
|
||||||
@@ -198,19 +226,27 @@ export default function AccountingPage() {
|
|||||||
// Fetch all purchases once, then filter/aggregate in-memory.
|
// Fetch all purchases once, then filter/aggregate in-memory.
|
||||||
const purchases = await purchasesApi.getAllPurchases({ per_page: 500 }, token);
|
const purchases = await purchasesApi.getAllPurchases({ per_page: 500 }, token);
|
||||||
setAllPurchases(purchases);
|
setAllPurchases(purchases);
|
||||||
setResult(computeAccounting(purchases, parseStoreRate(myketRate), parseStoreRate(cafeRate)));
|
setResult(
|
||||||
|
computeAccounting(
|
||||||
|
purchases,
|
||||||
|
parseStoreRate(myketRate),
|
||||||
|
parseStoreRate(cafeRate),
|
||||||
|
parseFee(transactionFee)
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'خطا در دریافت دادهها');
|
setError(err instanceof Error ? err.message : 'خطا در دریافت دادهها');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [token, myketRate, cafeRate]);
|
}, [token, myketRate, cafeRate, transactionFee]);
|
||||||
|
|
||||||
// Recompute whenever filters or commission rates change (uses cached data)
|
// Recompute whenever filters or commission rates change (uses cached data)
|
||||||
const runAnalysis = useCallback(() => {
|
const runAnalysis = useCallback(() => {
|
||||||
if (!allPurchases) return;
|
if (!allPurchases) return;
|
||||||
const myket = parseStoreRate(myketRate);
|
const myket = parseStoreRate(myketRate);
|
||||||
const cafe = parseStoreRate(cafeRate);
|
const cafe = parseStoreRate(cafeRate);
|
||||||
|
const fee = parseFee(transactionFee);
|
||||||
|
|
||||||
const filtered = allPurchases.filter((p) => {
|
const filtered = allPurchases.filter((p) => {
|
||||||
// Status
|
// Status
|
||||||
@@ -227,14 +263,28 @@ export default function AccountingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Date range (created_at is "Y-m-d H:i:s")
|
// Date range (created_at is "Y-m-d H:i:s")
|
||||||
if (dateFrom && p.created_at && p.created_at.slice(0, 10) < dateFrom) return false;
|
// When time is also selected, compare full datetime strings;
|
||||||
if (dateTo && p.created_at && p.created_at.slice(0, 10) > dateTo) return false;
|
// otherwise compare only the date portion (YYYY-MM-DD).
|
||||||
|
const created = p.created_at ?? '';
|
||||||
|
if (dateFrom) {
|
||||||
|
const datePart = created.slice(0, 10);
|
||||||
|
if (timeFrom) {
|
||||||
|
// Full datetime comparison: "YYYY-MM-DD HH:mm:ss"
|
||||||
|
if (created < `${dateFrom} ${timeFrom}`) return false;
|
||||||
|
} else if (datePart < dateFrom) return false;
|
||||||
|
}
|
||||||
|
if (dateTo) {
|
||||||
|
const datePart = created.slice(0, 10);
|
||||||
|
if (timeTo) {
|
||||||
|
if (created > `${dateTo} ${timeTo}`) return false;
|
||||||
|
} else if (datePart > dateTo) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
setResult(computeAccounting(filtered, myket, cafe));
|
setResult(computeAccounting(filtered, myket, cafe, fee));
|
||||||
}, [allPurchases, statuses, selectedProductId, selectedPackageName, dateFrom, dateTo, myketRate, cafeRate]);
|
}, [allPurchases, statuses, selectedProductId, selectedPackageName, dateFrom, dateTo, timeFrom, timeTo, myketRate, cafeRate, transactionFee]);
|
||||||
|
|
||||||
// Recompute on filter/rate changes (no refetch needed once data is cached)
|
// Recompute on filter/rate changes (no refetch needed once data is cached)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -246,9 +296,12 @@ export default function AccountingPage() {
|
|||||||
setSelectedProductId('');
|
setSelectedProductId('');
|
||||||
setDateFrom('');
|
setDateFrom('');
|
||||||
setDateTo('');
|
setDateTo('');
|
||||||
|
setTimeFrom('');
|
||||||
|
setTimeTo('');
|
||||||
setStatuses([...PAID_STATUSES]);
|
setStatuses([...PAID_STATUSES]);
|
||||||
setMyketRate('15');
|
setMyketRate('85');
|
||||||
setCafeRate('15');
|
setCafeRate('85');
|
||||||
|
setTransactionFee('1200');
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleStatus = (code: number) => {
|
const toggleStatus = (code: number) => {
|
||||||
@@ -290,7 +343,7 @@ export default function AccountingPage() {
|
|||||||
}}
|
}}
|
||||||
className="bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800 rounded-2xl p-5 sm:p-6 mb-6 shadow-sm"
|
className="bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800 rounded-2xl p-5 sm:p-6 mb-6 shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="package_name" className={labelClass}>پکیج</label>
|
<label htmlFor="package_name" className={labelClass}>پکیج</label>
|
||||||
<select
|
<select
|
||||||
@@ -335,6 +388,18 @@ export default function AccountingPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="time_from" className={labelClass}>از ساعت</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
id="time_from"
|
||||||
|
value={timeFrom}
|
||||||
|
onChange={(e) => setTimeFrom(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="۰۰:۰۰"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PersianDatePicker
|
<PersianDatePicker
|
||||||
id="date_to"
|
id="date_to"
|
||||||
@@ -344,6 +409,18 @@ export default function AccountingPage() {
|
|||||||
onChange={setDateTo}
|
onChange={setDateTo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="time_to" className={labelClass}>تا ساعت</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
id="time_to"
|
||||||
|
value={timeTo}
|
||||||
|
onChange={(e) => setTimeTo(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="۲۳:۵۹"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status checkboxes */}
|
{/* Status checkboxes */}
|
||||||
@@ -368,11 +445,11 @@ export default function AccountingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Commission rates */}
|
{/* Commission settings */}
|
||||||
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="myket_rate" className={labelClass}>
|
<label htmlFor="myket_rate" className={labelClass}>
|
||||||
سهم مایکت (%)
|
سهم توسعهدهنده مایکت (٪)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -381,12 +458,12 @@ export default function AccountingPage() {
|
|||||||
value={myketRate}
|
value={myketRate}
|
||||||
onChange={(e) => setMyketRate(e.target.value)}
|
onChange={(e) => setMyketRate(e.target.value)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="۱۵"
|
placeholder="۸۵"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cafe_rate" className={labelClass}>
|
<label htmlFor="cafe_rate" className={labelClass}>
|
||||||
سهم کافه بازار (%)
|
سهم توسعهدهنده کافه بازار (٪)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -395,12 +472,27 @@ export default function AccountingPage() {
|
|||||||
value={cafeRate}
|
value={cafeRate}
|
||||||
onChange={(e) => setCafeRate(e.target.value)}
|
onChange={(e) => setCafeRate(e.target.value)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="۱۵"
|
placeholder="۸۵"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="transaction_fee" className={labelClass}>
|
||||||
|
کارمزد تراکنش (ریال)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="transaction_fee"
|
||||||
|
inputMode="numeric"
|
||||||
|
value={transactionFee}
|
||||||
|
onChange={(e) => setTransactionFee(e.target.value)}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="۱۲۰۰"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<p className="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
مایکت معمولاً ۱۵٪ یا ۳۰٪ و کافه بازار ۱۵٪ (زیر یک میلیارد تومان) یا ۳۰٪ کارمزد میگیرد.
|
سهم توسعهدهنده = (قیمت ÷ ۱٫۱ − کارمزد تراکنش) × درصد سهم. پیشفرض مایکت و
|
||||||
|
کافهبازار ۸۵٪ (فروش زیر ۱۰ میلیارد ریال) و ۷۰٪ (بالای آن) است.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 mt-5">
|
<div className="flex items-center gap-3 mt-5">
|
||||||
@@ -566,7 +658,7 @@ function StoreCard({ title, summary }: { title: string; summary: StoreSummary })
|
|||||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{title}</h4>
|
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{title}</h4>
|
||||||
{summary.rate > 0 && (
|
{summary.rate > 0 && (
|
||||||
<span className="px-2 py-0.5 text-xs font-medium rounded-lg bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-400">
|
<span className="px-2 py-0.5 text-xs font-medium rounded-lg bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-400">
|
||||||
کارمزد {summary.rate}٪
|
سهم توسعهدهنده {summary.rate}٪
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -580,8 +672,8 @@ function StoreCard({ title, summary }: { title: string; summary: StoreSummary })
|
|||||||
<span className="text-gray-900 dark:text-gray-100 font-medium">{formatPrice(summary.gross)} تومان</span>
|
<span className="text-gray-900 dark:text-gray-100 font-medium">{formatPrice(summary.gross)} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-gray-500 dark:text-gray-400">
|
<div className="flex justify-between text-gray-500 dark:text-gray-400">
|
||||||
<span>کارمزد</span>
|
<span>سهم فروشگاهها</span>
|
||||||
<span className="text-amber-600 dark:text-amber-400 font-medium">{formatPrice(summary.commission)} تومان</span>
|
<span className="text-amber-600 dark:text-amber-400 font-medium">{formatPrice(summary.fee)} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between pt-1.5 border-t border-gray-100 dark:border-gray-800 text-gray-500 dark:text-gray-400">
|
<div className="flex justify-between pt-1.5 border-t border-gray-100 dark:border-gray-800 text-gray-500 dark:text-gray-400">
|
||||||
<span>سود خالص</span>
|
<span>سود خالص</span>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
getPurchaseStatusLabel,
|
getPurchaseStatusLabel,
|
||||||
} from '@/types/purchase';
|
} from '@/types/purchase';
|
||||||
import { PackageName } from '@/types/package';
|
import { PackageName } from '@/types/package';
|
||||||
import { formatPrice, formatDate, getProductTypeLabel, getPurchaseFinalPrice } from '@/lib/utils';
|
import { formatPrice, formatDate, formatTime, getProductTypeLabel, getPurchaseFinalPrice } from '@/lib/utils';
|
||||||
import { MagnifyingGlassIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
import { MagnifyingGlassIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
const statusBadgeClass = (status: number): string => {
|
const statusBadgeClass = (status: number): string => {
|
||||||
@@ -212,7 +212,7 @@ export default function PurchasesPage() {
|
|||||||
<th className="px-4 py-3">قیمت نهایی</th>
|
<th className="px-4 py-3">قیمت نهایی</th>
|
||||||
<th className="px-4 py-3">منبع پرداخت</th>
|
<th className="px-4 py-3">منبع پرداخت</th>
|
||||||
<th className="px-4 py-3">وضعیت</th>
|
<th className="px-4 py-3">وضعیت</th>
|
||||||
<th className="px-4 py-3">تاریخ</th>
|
<th className="px-4 py-3">تاریخ و ساعت</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
@@ -254,7 +254,7 @@ export default function PurchasesPage() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3.5 text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="px-4 py-3.5 text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
{formatDate(purchase.created_at)}
|
{formatDate(purchase.created_at)} {formatTime(purchase.created_at)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ export const parseStoreRate = (value: string | number): number => {
|
|||||||
return Math.min(100, Math.max(0, n));
|
return Math.min(100, Math.max(0, n));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Parse a non-negative integer input (e.g. transaction fee in Rial).
|
||||||
|
export const parseFee = (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.max(0, n);
|
||||||
|
};
|
||||||
|
|
||||||
export const formatDate = (dateString: string | null): string => {
|
export const formatDate = (dateString: string | null): string => {
|
||||||
if (!dateString) return 'نامشخص';
|
if (!dateString) return 'نامشخص';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user