feat(representation): enhance finance page with summary cards and improved table styling

This commit is contained in:
hamed
2026-06-24 20:16:10 +03:30
parent 9603b702c1
commit 93fa0fc2c9
2 changed files with 108 additions and 26 deletions
+106 -26
View File
@@ -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>
+2
View File
@@ -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 {