adds filter in org commission distribution panel

This commit is contained in:
Satyam Rastogi
2026-05-15 18:50:22 +05:30
parent a85b90d283
commit 2796d2ddb8
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo, useState, useRef, useEffect } from 'react';
import { DollarSign, Users } from 'lucide-react'; import { DollarSign, Users, Search, ChevronDown, X, Check, FilterX } from 'lucide-react';
const COMMISSION_TYPES = { const COMMISSION_TYPES = {
percent_gross: '% of Gross', percent_gross: '% of Gross',
@@ -90,21 +90,241 @@ const CommissionDistributionPanel = ({
return entries; return entries;
}, [ownerProjects, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults]); }, [ownerProjects, userCommissionOverrides, roleCommissionOverrides, orgMembers, orgCommissionDefaults]);
const total = useMemo(() => rows.reduce((s, r) => s + r.amount, 0), [rows]); // ── Filter state ───────────────────────────────────────────────
const [nameQuery, setNameQuery] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const [projectFilter, setProjectFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
// Searchable project dropdown
const [projectOpen, setProjectOpen] = useState(false);
const [projectQuery, setProjectQuery] = useState('');
const projectBoxRef = useRef(null);
useEffect(() => {
if (!projectOpen) return;
const onClick = (e) => {
if (projectBoxRef.current && !projectBoxRef.current.contains(e.target)) {
setProjectOpen(false);
setProjectQuery('');
}
};
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}, [projectOpen]);
// Derive available roles, projects, and commission types from current data
const roleOptions = useMemo(() => {
const set = new Set(rows.map(r => r.role).filter(Boolean));
return Array.from(set).sort();
}, [rows]);
const projectOptions = useMemo(() => {
const map = new Map();
for (const r of rows) {
if (!map.has(r.projectId)) {
map.set(r.projectId, { id: r.projectId, label: r.projectLabel });
}
}
return Array.from(map.values());
}, [rows]);
const typeOptions = useMemo(() => {
const set = new Set(rows.map(r => r.commissionType).filter(Boolean));
return Array.from(set).sort();
}, [rows]);
const filteredProjectOptions = useMemo(() => {
const q = projectQuery.trim().toLowerCase();
if (!q) return projectOptions;
return projectOptions.filter(p =>
(p.label || '').toLowerCase().includes(q) ||
(p.id || '').toLowerCase().includes(q)
);
}, [projectOptions, projectQuery]);
const selectedProjectLabel = useMemo(() => {
if (projectFilter === 'all') return 'All Projects';
const p = projectOptions.find(p => p.id === projectFilter);
return p ? `${p.label}${p.id}` : 'All Projects';
}, [projectFilter, projectOptions]);
const filteredRows = useMemo(() => {
const q = nameQuery.trim().toLowerCase();
return rows.filter(r => {
if (q && !(r.name || '').toLowerCase().includes(q)) return false;
if (roleFilter !== 'all' && r.role !== roleFilter) return false;
if (projectFilter !== 'all' && r.projectId !== projectFilter) return false;
if (typeFilter !== 'all' && r.commissionType !== typeFilter) return false;
return true;
});
}, [rows, nameQuery, roleFilter, projectFilter, typeFilter]);
const total = useMemo(() => filteredRows.reduce((s, r) => s + r.amount, 0), [filteredRows]);
const hasActiveFilters =
nameQuery.trim() !== '' || roleFilter !== 'all' || projectFilter !== 'all' || typeFilter !== 'all';
const clearFilters = () => {
setNameQuery('');
setRoleFilter('all');
setProjectFilter('all');
setTypeFilter('all');
setProjectQuery('');
};
const selectClass =
"appearance-none w-full pl-3 pr-8 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-colors cursor-pointer";
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 flex-wrap">
<DollarSign className="text-emerald-500 dark:text-emerald-400 drop-shadow-[0_0_8px_rgba(16,185,129,0.4)]" size={20} /> <DollarSign className="text-emerald-500 dark:text-emerald-400 drop-shadow-[0_0_8px_rgba(16,185,129,0.4)]" size={20} />
<h3 className="text-sm font-mono font-bold text-zinc-900 dark:text-white tracking-widest uppercase"> <h3 className="text-sm font-mono font-bold text-zinc-900 dark:text-white tracking-widest uppercase">
Commission Distribution Commission Distribution
</h3> </h3>
<span className="text-[10px] font-mono text-zinc-500 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 rounded-full px-2.5 py-0.5"> <span className="text-[10px] font-mono text-zinc-500 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 rounded-full px-2.5 py-0.5">
{rows.length} entr{rows.length === 1 ? 'y' : 'ies'} across {ownerProjects.length} project{ownerProjects.length !== 1 ? 's' : ''} {hasActiveFilters
? `${filteredRows.length} of ${rows.length} entr${rows.length === 1 ? 'y' : 'ies'}`
: `${rows.length} entr${rows.length === 1 ? 'y' : 'ies'} across ${ownerProjects.length} project${ownerProjects.length !== 1 ? 's' : ''}`
}
</span> </span>
</div> </div>
{/* Filter bar */}
{rows.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2.5 p-3 rounded-xl bg-zinc-50 dark:bg-white/[0.02] border border-zinc-200 dark:border-white/5">
{/* Search by Name */}
<div className="relative">
<label htmlFor="commission-name-search" className="sr-only">Search by name</label>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" size={14} />
<input
id="commission-name-search"
type="text"
placeholder="Search by name..."
value={nameQuery}
onChange={(e) => setNameQuery(e.target.value)}
className="w-full bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 rounded-xl py-2 pl-9 pr-8 text-sm text-zinc-900 dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-500 outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40"
/>
{nameQuery && (
<button
type="button"
onClick={() => setNameQuery('')}
aria-label="Clear name search"
className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200"
>
<X size={14} />
</button>
)}
</div>
{/* Role filter */}
<div className="relative">
<label htmlFor="commission-role-filter" className="sr-only">Filter by role</label>
<select
id="commission-role-filter"
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className={selectClass}
>
<option value="all">All Roles</option>
{roleOptions.map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
</div>
{/* Project searchable dropdown */}
<div className="relative" ref={projectBoxRef}>
<button
type="button"
onClick={() => setProjectOpen(o => !o)}
className="w-full flex items-center justify-between gap-2 pl-3 pr-2 py-2 text-sm rounded-xl bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/10 text-zinc-900 dark:text-white outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-colors"
>
<span className="truncate text-left">{selectedProjectLabel}</span>
<ChevronDown size={14} className={`shrink-0 text-zinc-400 transition-transform ${projectOpen ? 'rotate-180' : ''}`} />
</button>
{projectOpen && (
<div className="absolute z-30 left-0 right-0 mt-1 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 shadow-xl shadow-black/10 dark:shadow-black/50 overflow-hidden">
<div className="relative p-2 border-b border-zinc-100 dark:border-white/5">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" size={14} />
<input
autoFocus
type="text"
placeholder="Search projects..."
value={projectQuery}
onChange={(e) => setProjectQuery(e.target.value)}
className="w-full bg-zinc-50 dark:bg-black/40 border border-zinc-200 dark:border-white/10 rounded-lg py-1.5 pl-8 pr-2 text-sm text-zinc-900 dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-500 outline-none"
/>
</div>
<div className="max-h-56 overflow-y-auto custom-scrollbar py-1">
<button
type="button"
onClick={() => { setProjectFilter('all'); setProjectOpen(false); setProjectQuery(''); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${projectFilter === 'all' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span>All Projects</span>
{projectFilter === 'all' && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
{filteredProjectOptions.length === 0 ? (
<p className="px-3 py-2 text-xs text-zinc-400 dark:text-zinc-500">No projects match "{projectQuery}"</p>
) : (
filteredProjectOptions.map(p => {
const isSelected = p.id === projectFilter;
return (
<button
key={p.id}
type="button"
onClick={() => { setProjectFilter(p.id); setProjectOpen(false); setProjectQuery(''); }}
className={`w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors ${isSelected ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10' : 'text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-white/5'}`}
>
<span className="truncate">
<span className="font-semibold">{p.label}</span>
<span className="ml-2 text-zinc-400 dark:text-zinc-500 font-mono text-[11px]">{p.id}</span>
</span>
{isSelected && <Check size={12} strokeWidth={2.5} className="shrink-0 opacity-70" />}
</button>
);
})
)}
</div>
</div>
)}
</div>
{/* Commission Type filter */}
<div className="relative">
<label htmlFor="commission-type-filter" className="sr-only">Filter by commission type</label>
<select
id="commission-type-filter"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className={selectClass}
>
<option value="all">All Commission Types</option>
{typeOptions.map(t => (
<option key={t} value={t}>{t}</option>
))}
</select>
<ChevronDown size={14} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none" />
</div>
{hasActiveFilters && (
<div className="sm:col-span-2 lg:col-span-4 flex justify-end">
<button
type="button"
onClick={clearFilters}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white bg-zinc-100 dark:bg-white/5 hover:bg-zinc-200 dark:hover:bg-white/10 border border-zinc-200 dark:border-white/10 transition-colors"
>
<FilterX size={12} /> Clear Filters
</button>
</div>
)}
</div>
)}
{rows.length === 0 ? ( {rows.length === 0 ? (
<div className="py-10 text-center"> <div className="py-10 text-center">
<Users size={28} className="mx-auto mb-3 text-zinc-400" /> <Users size={28} className="mx-auto mb-3 text-zinc-400" />
@@ -124,7 +344,22 @@ const CommissionDistributionPanel = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.map((row) => ( {filteredRows.length === 0 && (
<tr>
<td colSpan={5} className="px-4 py-10 text-center">
<Users size={22} className="mx-auto mb-2 text-zinc-400" />
<p className="text-sm text-zinc-500">No commission entries match the current filters.</p>
<button
type="button"
onClick={clearFilters}
className="mt-3 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 transition-colors"
>
<FilterX size={12} /> Clear Filters
</button>
</td>
</tr>
)}
{filteredRows.map((row) => (
<tr key={row.key} className="border-b border-zinc-100 dark:border-white/[0.03] hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors"> <tr key={row.key} className="border-b border-zinc-100 dark:border-white/[0.03] hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
@@ -160,7 +395,7 @@ const CommissionDistributionPanel = ({
<tfoot className="border-t-2 border-zinc-300 dark:border-white/10"> <tfoot className="border-t-2 border-zinc-300 dark:border-white/10">
<tr className="bg-zinc-50 dark:bg-white/[0.03]"> <tr className="bg-zinc-50 dark:bg-white/[0.03]">
<td colSpan={2} className="px-4 py-3 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest"> <td colSpan={2} className="px-4 py-3 text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-widest">
Total Commission Amount {hasActiveFilters ? 'Filtered Commission Total' : 'Total Commission Amount'}
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<span className="font-mono text-sm font-extrabold text-green-600 dark:text-[#39ff14]"> <span className="font-mono text-sm font-extrabold text-green-600 dark:text-[#39ff14]">