mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
refactor(cabinet): unify all statistics onto the shared StatCard
Bring every stat strip across the admin panel and user cabinet onto the canonical StatCard (icon chip + label-on-top + tone-coloured value), matching the sales/Remnawave style the rest of the app already used. - Convert ~20 hand-rolled stat strips across 21 pages/components - Remove 4 divergent local StatCard components (Dashboard, Users, BanSystem, Payments) and migrate Remnawave's local StatCard (33 cards) - Extend StatCard with optional subValue, loading skeleton, trailing slot, and icon-size normalisation (all backward-compatible) - StatsGrid nav tiles now use StatCard while keeping link/chevron/loading - Preserve all data, i18n keys, conditionals and interactive filters; fix the dead 'info' colour by mapping it to accent
This commit is contained in:
@@ -7,12 +7,16 @@ import { DateField } from '../components/DateField';
|
||||
import { useCurrency } from '../hooks/useCurrency';
|
||||
import type { PendingPayment, PaginatedResponse } from '../types';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { StatCard } from '@/components/stats';
|
||||
import {
|
||||
BackIcon,
|
||||
SearchIcon,
|
||||
CalendarIcon,
|
||||
RefreshIcon,
|
||||
CheckCircleIcon,
|
||||
ChartBarIcon,
|
||||
ClockIcon,
|
||||
XCircleIcon,
|
||||
} from '@/components/icons';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
@@ -38,38 +42,6 @@ function StatusBadge({ status }: StatusBadgeProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: number;
|
||||
color: 'blue' | 'amber' | 'green' | 'red';
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color, isActive, onClick }: StatCardProps) {
|
||||
const colors: Record<string, string> = {
|
||||
blue: 'border-accent-500/30 bg-accent-500/20 text-accent-400',
|
||||
amber: 'border-warning-500/30 bg-warning-500/20 text-warning-400',
|
||||
green: 'border-success-500/30 bg-success-500/20 text-success-400',
|
||||
red: 'border-error-500/30 bg-error-500/20 text-error-400',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded-xl border p-4 text-left transition-all ${
|
||||
isActive
|
||||
? colors[color]
|
||||
: 'border-dark-700/50 bg-dark-800/50 text-dark-300 hover:border-dark-600'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-2xl font-bold ${isActive ? '' : 'text-dark-50'}`}>{value}</div>
|
||||
<div className="text-sm opacity-80">{label}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminPayments() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -353,34 +325,62 @@ export default function AdminPayments() {
|
||||
{/* Stats cards */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard
|
||||
label={t('admin.payments.totalCount')}
|
||||
value={stats.total}
|
||||
color="blue"
|
||||
isActive={statusFilter === 'all'}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStatusCardClick('all')}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('admin.payments.pendingCount')}
|
||||
value={stats.pending}
|
||||
color="amber"
|
||||
isActive={statusFilter === 'pending'}
|
||||
className={`rounded-xl text-left transition-all ${
|
||||
statusFilter === 'all' ? 'ring-2 ring-accent-500' : ''
|
||||
}`}
|
||||
>
|
||||
<StatCard
|
||||
label={t('admin.payments.totalCount')}
|
||||
value={stats.total}
|
||||
icon={<ChartBarIcon className="h-5 w-5" />}
|
||||
tone="accent"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStatusCardClick('pending')}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('admin.payments.paidCount')}
|
||||
value={stats.paid}
|
||||
color="green"
|
||||
isActive={statusFilter === 'paid'}
|
||||
className={`rounded-xl text-left transition-all ${
|
||||
statusFilter === 'pending' ? 'ring-2 ring-warning-500' : ''
|
||||
}`}
|
||||
>
|
||||
<StatCard
|
||||
label={t('admin.payments.pendingCount')}
|
||||
value={stats.pending}
|
||||
icon={<ClockIcon className="h-5 w-5" />}
|
||||
tone="warning"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStatusCardClick('paid')}
|
||||
/>
|
||||
<StatCard
|
||||
label={t('admin.payments.cancelledCount')}
|
||||
value={stats.cancelled}
|
||||
color="red"
|
||||
isActive={statusFilter === 'cancelled'}
|
||||
className={`rounded-xl text-left transition-all ${
|
||||
statusFilter === 'paid' ? 'ring-2 ring-success-500' : ''
|
||||
}`}
|
||||
>
|
||||
<StatCard
|
||||
label={t('admin.payments.paidCount')}
|
||||
value={stats.paid}
|
||||
icon={<CheckCircleIcon className="h-5 w-5" />}
|
||||
tone="success"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStatusCardClick('cancelled')}
|
||||
/>
|
||||
className={`rounded-xl text-left transition-all ${
|
||||
statusFilter === 'cancelled' ? 'ring-2 ring-error-500' : ''
|
||||
}`}
|
||||
>
|
||||
<StatCard
|
||||
label={t('admin.payments.cancelledCount')}
|
||||
value={stats.cancelled}
|
||||
icon={<XCircleIcon className="h-5 w-5" />}
|
||||
tone="error"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user