Sprint 2 - Sales Frontend: - Add frontend services, hooks, and pages for Sales module - Update router with Sales routes Sprint 3 - Commissions Backend: - Add Commissions module with entities, DTOs, services, controllers - Add DDL schema for commissions tables - Register CommissionsModule in AppModule Sprint 4 - Commissions Frontend: - Add frontend services, hooks, and pages for Commissions module - Add dashboard, schemes, entries, periods, and my-earnings pages - Update router with Commissions routes Update submodule references: backend, database, frontend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
135 lines
5.8 KiB
TypeScript
135 lines
5.8 KiB
TypeScript
import { DollarSign, Users, Calendar, TrendingUp, Clock, CheckCircle } from 'lucide-react';
|
|
import { DashboardSummary, TopEarner, PeriodEarnings } from '../../services/commissions/dashboard.api';
|
|
|
|
interface CommissionsDashboardProps {
|
|
summary: DashboardSummary;
|
|
topEarners: TopEarner[];
|
|
periodEarnings: PeriodEarnings[];
|
|
}
|
|
|
|
export function CommissionsDashboard({ summary, topEarners, periodEarnings }: CommissionsDashboardProps) {
|
|
const formatCurrency = (amount: number) =>
|
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: summary.currency }).format(amount);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Pending Commissions</p>
|
|
<p className="text-2xl font-bold text-yellow-600">{formatCurrency(summary.total_pending)}</p>
|
|
<p className="text-xs text-gray-400">{summary.pending_count} entries</p>
|
|
</div>
|
|
<div className="p-3 bg-yellow-100 rounded-full">
|
|
<Clock className="h-6 w-6 text-yellow-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Approved</p>
|
|
<p className="text-2xl font-bold text-green-600">{formatCurrency(summary.total_approved)}</p>
|
|
<p className="text-xs text-gray-400">{summary.approved_count} entries</p>
|
|
</div>
|
|
<div className="p-3 bg-green-100 rounded-full">
|
|
<CheckCircle className="h-6 w-6 text-green-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Total Paid</p>
|
|
<p className="text-2xl font-bold text-blue-600">{formatCurrency(summary.total_paid)}</p>
|
|
<p className="text-xs text-gray-400">{summary.paid_count} entries</p>
|
|
</div>
|
|
<div className="p-3 bg-blue-100 rounded-full">
|
|
<DollarSign className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Active Schemes</p>
|
|
<p className="text-2xl font-bold text-purple-600">{summary.active_schemes}</p>
|
|
<p className="text-xs text-gray-400">{summary.active_assignments} assignments</p>
|
|
</div>
|
|
<div className="p-3 bg-purple-100 rounded-full">
|
|
<TrendingUp className="h-6 w-6 text-purple-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Top Earners */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-semibold mb-4">Top Earners</h3>
|
|
{topEarners.length === 0 ? (
|
|
<p className="text-gray-500 text-center py-4">No earners data</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{topEarners.map((earner) => (
|
|
<div key={earner.user_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold">
|
|
{earner.rank}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{earner.user_name}</p>
|
|
<p className="text-sm text-gray-500">{earner.entries_count} sales</p>
|
|
</div>
|
|
</div>
|
|
<p className="font-bold text-green-600">{formatCurrency(earner.total_earned)}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recent Periods */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-semibold mb-4">Recent Periods</h3>
|
|
{periodEarnings.length === 0 ? (
|
|
<p className="text-gray-500 text-center py-4">No periods data</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{periodEarnings.slice(0, 5).map((period) => (
|
|
<div key={period.period_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<p className="font-medium">{period.period_name}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{new Date(period.starts_at).toLocaleDateString()} - {new Date(period.ends_at).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-bold">{formatCurrency(period.total_amount)}</p>
|
|
<span
|
|
className={`text-xs px-2 py-1 rounded-full ${
|
|
period.status === 'paid'
|
|
? 'bg-green-100 text-green-700'
|
|
: period.status === 'open'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'bg-gray-100 text-gray-700'
|
|
}`}
|
|
>
|
|
{period.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|