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 { useQuery } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
BanknotesIcon, ReceiptPercentIcon, ChatBubbleLeftRightIcon,
|
||||||
|
CalculatorIcon, ArrowPathIcon, DocumentTextIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { ApiResponse, PaginatedResponse } 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';
|
import Pagination from '../components/ui/Pagination';
|
||||||
|
|
||||||
interface FinanceRow {
|
interface FinanceRow {
|
||||||
@@ -27,16 +31,19 @@ function rangeFrom(key: RangeKey): number | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RANGE_LABEL: Record<RangeKey, string> = {
|
const RANGES: { key: RangeKey; label: string }[] = [
|
||||||
today: 'امروز', week: 'این هفته', month: 'این ماه', all: 'همه',
|
{ key: 'today', label: 'امروز' },
|
||||||
};
|
{ key: 'week', label: '۷ روز اخیر' },
|
||||||
|
{ key: 'month', label: '۳۰ روز اخیر' },
|
||||||
|
{ key: 'all', label: 'همه' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function RepresentationFinancePage() {
|
export default function RepresentationFinancePage() {
|
||||||
const [range, setRange] = useState<RangeKey>('month');
|
const [range, setRange] = useState<RangeKey>('month');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const limit = 15;
|
const limit = 15;
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading, isFetching, refetch } = useQuery({
|
||||||
queryKey: ['representation-finance', range, page],
|
queryKey: ['representation-finance', range, page],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||||
@@ -49,48 +56,121 @@ export default function RepresentationFinancePage() {
|
|||||||
const items: FinanceRow[] = data?.data ?? [];
|
const items: FinanceRow[] = data?.data ?? [];
|
||||||
const total = data?.meta?.totalRecords ?? 0;
|
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 (
|
return (
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="section-title">گزارش مالی</h1>
|
<h1 className="section-title">گزارش مالی</h1>
|
||||||
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>درآمد ثبتشده از پورسانت نوبتها</div>
|
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>
|
||||||
|
درآمد ثبتشده از پورسانت نوبتها — تفکیک هر تراکنش
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="btn ghost sm" onClick={() => refetch()} disabled={isFetching}>
|
||||||
|
<ArrowPathIcon style={{ width: 14, height: 14 }} />
|
||||||
|
بهروزرسانی
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--gap)' }}>
|
{/* فیلتر بازه — segmented */}
|
||||||
{(['today', 'week', 'month', 'all'] as RangeKey[]).map(k => (
|
<div className="seg" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<button key={k} className={range === k ? 'on' : ''} onClick={() => { setRange(k); setPage(1); }}>
|
{RANGES.map(r => (
|
||||||
{RANGE_LABEL[k]}
|
<button
|
||||||
|
key={r.key}
|
||||||
|
className={range === r.key ? 'on' : ''}
|
||||||
|
onClick={() => { setRange(r.key); setPage(1); }}
|
||||||
|
>
|
||||||
|
{r.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table className="tbl" style={{ width: '100%' }}>
|
<table className="tbl tbl-zebra" style={{ width: '100%', minWidth: 720 }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{isLoading && (
|
{isLoading && Array.from({ length: 5 }).map((_, i) => (
|
||||||
<tr><td colSpan={7} className="muted" style={{ textAlign: 'center', padding: 16 }}>در حال بارگذاری...</td></tr>
|
<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 && (
|
{!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}>
|
<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.gross_rials)}</td>
|
||||||
<td>{formatRial(r.tax_rials)}</td>
|
<td className="muted">{formatRial(r.tax_rials)}</td>
|
||||||
<td>{formatRial(r.sms_fee_rials)}</td>
|
<td className="muted">{formatRial(r.sms_fee_rials)}</td>
|
||||||
<td>{r.commission_percent}٪</td>
|
<td><span className="badge gray">{formatNumber(r.commission_percent)}٪</span></td>
|
||||||
<td>{formatRial(r.representation_share_rials)}</td>
|
<td style={{ fontWeight: 700, color: 'var(--primary)' }}>{formatRial(r.representation_share_rials)}</td>
|
||||||
<td>{formatDate(r.created_at)}</td>
|
<td className="muted" style={{ whiteSpace: 'nowrap' }}>{formatDate(r.created_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</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 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 td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
||||||
.tbl tbody tr:last-child td { border-bottom: none; }
|
.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 ───────────────────────────────── */
|
/* ── Appointment status badges ───────────────────────────────── */
|
||||||
.appt-status {
|
.appt-status {
|
||||||
|
|||||||
Reference in New Issue
Block a user