feat(representation): enhance finance page with summary cards and improved table styling
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
BanknotesIcon, ReceiptPercentIcon, ChatBubbleLeftRightIcon,
|
||||
CalculatorIcon, ArrowPathIcon, DocumentTextIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { api } from '../lib/api';
|
||||
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||
import { formatRial, formatDate } from '../lib/utils';
|
||||
import { formatRial, formatNumber, formatDate } from '../lib/utils';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
|
||||
interface FinanceRow {
|
||||
@@ -27,16 +31,19 @@ function rangeFrom(key: RangeKey): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const RANGE_LABEL: Record<RangeKey, string> = {
|
||||
today: 'امروز', week: 'این هفته', month: 'این ماه', all: 'همه',
|
||||
};
|
||||
const RANGES: { key: RangeKey; label: string }[] = [
|
||||
{ key: 'today', label: 'امروز' },
|
||||
{ key: 'week', label: '۷ روز اخیر' },
|
||||
{ key: 'month', label: '۳۰ روز اخیر' },
|
||||
{ key: 'all', label: 'همه' },
|
||||
];
|
||||
|
||||
export default function RepresentationFinancePage() {
|
||||
const [range, setRange] = useState<RangeKey>('month');
|
||||
const [page, setPage] = useState(1);
|
||||
const limit = 15;
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
const { data, isLoading, isFetching, refetch } = useQuery({
|
||||
queryKey: ['representation-finance', range, page],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
@@ -49,48 +56,121 @@ export default function RepresentationFinancePage() {
|
||||
const items: FinanceRow[] = data?.data ?? [];
|
||||
const total = data?.meta?.totalRecords ?? 0;
|
||||
|
||||
// جمعِ ردیفهای صفحهی جاری (نمایش سریع؛ دقیق برای همین صفحه).
|
||||
const pageSums = useMemo(() => items.reduce(
|
||||
(acc, r) => {
|
||||
acc.share += r.representation_share_rials;
|
||||
acc.gross += r.gross_rials;
|
||||
acc.tax += r.tax_rials;
|
||||
acc.sms += r.sms_fee_rials;
|
||||
return acc;
|
||||
},
|
||||
{ share: 0, gross: 0, tax: 0, sms: 0 },
|
||||
), [items]);
|
||||
|
||||
const cards = [
|
||||
{ label: 'سهم نماینده (این صفحه)', value: formatRial(pageSums.share), icon: BanknotesIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' },
|
||||
{ label: 'مبلغ نوبتها', value: formatRial(pageSums.gross), icon: CalculatorIcon, color: 'var(--info)', bg: 'var(--info-bg)' },
|
||||
{ label: 'مالیات', value: formatRial(pageSums.tax), icon: ReceiptPercentIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' },
|
||||
{ label: 'هزینه پیامک', value: formatRial(pageSums.sms), icon: ChatBubbleLeftRightIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fade-in">
|
||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||
<div>
|
||||
<h1 className="section-title">گزارش مالی</h1>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>درآمد ثبتشده از پورسانت نوبتها</div>
|
||||
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>
|
||||
درآمد ثبتشده از پورسانت نوبتها — تفکیک هر تراکنش
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn ghost sm" onClick={() => refetch()} disabled={isFetching}>
|
||||
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||
بهروزرسانی
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
|
||||
{(['today', 'week', 'month', 'all'] as RangeKey[]).map(k => (
|
||||
<button key={k} className={range === k ? 'on' : ''} onClick={() => { setRange(k); setPage(1); }}>
|
||||
{RANGE_LABEL[k]}
|
||||
{/* فیلتر بازه — segmented */}
|
||||
<div className="seg" style={{ marginBottom: 'var(--gap)' }}>
|
||||
{RANGES.map(r => (
|
||||
<button
|
||||
key={r.key}
|
||||
className={range === r.key ? 'on' : ''}
|
||||
onClick={() => { setRange(r.key); setPage(1); }}
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="card card-pad">
|
||||
{/* کارتهای خلاصه */}
|
||||
<div className="stat-grid" style={{ gridTemplateColumns: 'repeat(4, 1fr)', marginBottom: 'var(--gap)' }}>
|
||||
{cards.map(c => (
|
||||
<div key={c.label} className="stat">
|
||||
<div className="ico" style={{ background: c.bg, color: c.color }}>
|
||||
<c.icon style={{ width: 20, height: 20 }} />
|
||||
</div>
|
||||
<div className="lbl">{c.label}</div>
|
||||
<div className="val" style={{ fontSize: 15, color: c.color }}>
|
||||
{isLoading ? '—' : c.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* جدول تراکنشها */}
|
||||
<div className="card" style={{ overflow: 'hidden' }}>
|
||||
<div className="card-pad" style={{ borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<h3 style={{ fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<DocumentTextIcon style={{ width: 18, height: 18, color: 'var(--text-3)' }} />
|
||||
تراکنشها
|
||||
</h3>
|
||||
{!isLoading && <span className="muted" style={{ fontSize: 12.5 }}>{formatNumber(total)} ردیف</span>}
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="tbl" style={{ width: '100%' }}>
|
||||
<table className="tbl tbl-zebra" style={{ width: '100%', minWidth: 720 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>پزشک</th><th>مبلغ نوبت</th><th>مالیات</th><th>پیامک</th>
|
||||
<th>درصد</th><th>سهم نماینده</th><th>تاریخ</th>
|
||||
<th>پزشک</th>
|
||||
<th>مبلغ نوبت</th>
|
||||
<th>مالیات</th>
|
||||
<th>هزینه پیامک</th>
|
||||
<th>درصد پورسانت</th>
|
||||
<th>سهم نماینده</th>
|
||||
<th>تاریخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading && (
|
||||
<tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 16 }}>در حال بارگذاری...</td></tr>
|
||||
)}
|
||||
{isLoading && Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i}>
|
||||
{Array.from({ length: 7 }).map((__, j) => (
|
||||
<td key={j}><div className="skeleton" style={{ height: 13, borderRadius: 6, width: `${50 + (j * 13) % 40}%` }} /></td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{!isLoading && items.length === 0 && (
|
||||
<tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 16 }}>درآمدی در این بازه ثبت نشده است</td></tr>
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 16px', color: 'var(--text-3)' }}>
|
||||
<BanknotesIcon style={{ width: 36, height: 36, margin: '0 auto 10px', opacity: .4 }} />
|
||||
<div style={{ fontSize: 14 }}>در این بازه درآمدی ثبت نشده است</div>
|
||||
<div style={{ fontSize: 12.5, marginTop: 4 }}>با ثبت و پرداخت نوبت از دامنهی شما، پورسانت اینجا نمایش داده میشود.</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map(r => (
|
||||
|
||||
{!isLoading && items.map(r => (
|
||||
<tr key={r.uuid}>
|
||||
<td>{r.doctor_name ?? '—'}</td>
|
||||
<td style={{ fontWeight: 500 }}>{r.doctor_name ?? '—'}</td>
|
||||
<td>{formatRial(r.gross_rials)}</td>
|
||||
<td>{formatRial(r.tax_rials)}</td>
|
||||
<td>{formatRial(r.sms_fee_rials)}</td>
|
||||
<td>{r.commission_percent}٪</td>
|
||||
<td>{formatRial(r.representation_share_rials)}</td>
|
||||
<td>{formatDate(r.created_at)}</td>
|
||||
<td className="muted">{formatRial(r.tax_rials)}</td>
|
||||
<td className="muted">{formatRial(r.sms_fee_rials)}</td>
|
||||
<td><span className="badge gray">{formatNumber(r.commission_percent)}٪</span></td>
|
||||
<td style={{ fontWeight: 700, color: 'var(--primary)' }}>{formatRial(r.representation_share_rials)}</td>
|
||||
<td className="muted" style={{ whiteSpace: 'nowrap' }}>{formatDate(r.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -456,6 +456,8 @@ body {
|
||||
.tbl th { text-align: right; padding: 8px 12px; color: var(--text-3); font-weight: 500; white-space: nowrap; }
|
||||
.tbl td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
||||
.tbl tbody tr:last-child td { border-bottom: none; }
|
||||
.tbl-zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
|
||||
.tbl-zebra tbody tr:hover td { background: var(--surface-3); }
|
||||
|
||||
/* ── Appointment status badges ───────────────────────────────── */
|
||||
.appt-status {
|
||||
|
||||
Reference in New Issue
Block a user