Fixed UI inconsistencies
This commit is contained in:
@@ -72,6 +72,7 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'beneficiaries' | 'compliance'>('beneficiaries');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||
const [compStatusFilter, setCompStatusFilter] = useState('all');
|
||||
|
||||
// Modal states
|
||||
const [isBenModalOpen, setIsBenModalOpen] = useState(false);
|
||||
@@ -144,7 +145,14 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
const createBenMutation = useMutation({
|
||||
mutationFn: (vals: BeneficiaryFormValues) => {
|
||||
if (!effectiveProjectId) throw new Error('No project selected');
|
||||
return createBeneficiary({ ...vals, projectId: effectiveProjectId });
|
||||
return createBeneficiary({
|
||||
projectId: effectiveProjectId,
|
||||
name: vals.name,
|
||||
location: vals.location,
|
||||
category: vals.category,
|
||||
targetCount: vals.targetCount,
|
||||
reachedCount: vals.reachedCount,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['beneficiaries'] });
|
||||
@@ -158,7 +166,14 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
const updateBenMutation = useMutation({
|
||||
mutationFn: (vals: BeneficiaryFormValues) => {
|
||||
if (!editingBen) throw new Error('No beneficiary selected');
|
||||
return updateBeneficiary({ ...vals, beneficiaryId: editingBen.id });
|
||||
return updateBeneficiary({
|
||||
beneficiaryId: editingBen.id,
|
||||
name: vals.name,
|
||||
location: vals.location,
|
||||
category: vals.category,
|
||||
targetCount: vals.targetCount,
|
||||
reachedCount: vals.reachedCount,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['beneficiaries'] });
|
||||
@@ -236,13 +251,17 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
// Filtered & Sorted Compliance Items
|
||||
const filteredComps = useMemo(() => {
|
||||
return complianceItems.filter((c) => {
|
||||
return (
|
||||
const matchesSearch =
|
||||
!searchTerm.trim() ||
|
||||
(c.title && c.title.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
(c.requirements && c.requirements.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
(c.requirements && c.requirements.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
const matchesStatus =
|
||||
compStatusFilter === 'all' ||
|
||||
(compStatusFilter === 'approved' && c.status === ApprovalStatus.Approved) ||
|
||||
(compStatusFilter === 'pending' && c.status !== ApprovalStatus.Approved);
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [complianceItems, searchTerm]);
|
||||
}, [complianceItems, searchTerm, compStatusFilter]);
|
||||
|
||||
const { sortedData: sortedComps, sortColumn: compSortCol, sortDirection: compSortDir, handleSort: handleCompSort } =
|
||||
useTableSort(filteredComps);
|
||||
@@ -265,7 +284,7 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
const compExportColumns: ExportColumn<ComplianceItem>[] = [
|
||||
{ header: 'Compliance Requirement', key: 'title' },
|
||||
{ header: 'Requirements Detail', key: 'requirements' },
|
||||
{ header: 'Status', accessor: (c) => (c.status === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') },
|
||||
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') },
|
||||
];
|
||||
|
||||
const handleOpenEditBen = (b: Beneficiary) => {
|
||||
@@ -310,60 +329,6 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 flex-wrap relative z-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
refetchBens();
|
||||
refetchComps();
|
||||
}}
|
||||
disabled={fetchingBens || fetchingComps}
|
||||
className="flex items-center gap-2 px-3.5 py-2.5 rounded-xl text-xs font-semibold bg-slate-900/90 text-slate-300 hover:text-white border border-slate-800 hover:bg-slate-800/80 transition cursor-pointer"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${fetchingBens || fetchingComps ? 'animate-spin text-blue-400' : ''}`} />
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
|
||||
{activeTab === 'beneficiaries' ? (
|
||||
<ExportButton
|
||||
data={sortedBens}
|
||||
columns={benExportColumns}
|
||||
filename="project_beneficiaries"
|
||||
title="Beneficiaries Register"
|
||||
/>
|
||||
) : (
|
||||
<ExportButton
|
||||
data={sortedComps}
|
||||
columns={compExportColumns}
|
||||
filename="compliance_items"
|
||||
title="Compliance Requirements"
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingBen(null);
|
||||
benForm.reset({ name: '', category: 'Youth & Skills', location: 'National', targetCount: 100, reachedCount: 0 });
|
||||
setIsBenModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-500 hover:to-indigo-500 shadow-lg shadow-blue-600/25 transition cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Register Beneficiary</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingComp(null);
|
||||
compForm.reset({ title: '', requirements: '', status: ApprovalStatus.Pending });
|
||||
setIsCompModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-semibold bg-slate-800/90 text-white hover:bg-slate-700/80 border border-slate-700 transition cursor-pointer"
|
||||
>
|
||||
<CheckSquare className="w-4 h-4 text-emerald-400" />
|
||||
<span>Add Compliance Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
@@ -413,67 +378,122 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs & Filters */}
|
||||
{/* Tabs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('beneficiaries')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${
|
||||
activeTab === 'beneficiaries'
|
||||
? 'bg-blue-600 text-white shadow-md shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>Beneficiaries List ({beneficiaries.length})</span>
|
||||
</button>
|
||||
<div className="flex border-b border-slate-800 bg-[#0d121f]/80 px-2 sm:px-4 rounded-2xl backdrop-blur-sm overflow-x-auto gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('beneficiaries')}
|
||||
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer whitespace-nowrap ${
|
||||
activeTab === 'beneficiaries'
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span>Beneficiaries Register ({beneficiaries.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('compliance')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition cursor-pointer ${
|
||||
activeTab === 'compliance'
|
||||
? 'bg-blue-600 text-white shadow-md shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-white hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<CheckSquare className="w-3.5 h-3.5" />
|
||||
<span>Compliance Items ({complianceItems.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<FolderKanban className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>Project:</span>
|
||||
<select
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="w-3.5 h-3.5 absolute left-3 top-2.5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 w-40 sm:w-56"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActiveTab('compliance')}
|
||||
className={`py-3.5 px-4 text-xs font-semibold border-b-2 transition-colors flex items-center gap-2 cursor-pointer whitespace-nowrap ${
|
||||
activeTab === 'compliance'
|
||||
? 'border-blue-500 text-blue-400'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<CheckSquare className="w-4 h-4" />
|
||||
<span>Compliance Requirements ({complianceItems.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab 1: Beneficiaries Table */}
|
||||
{/* Tab 1: Beneficiaries Tab Toolbar & Content */}
|
||||
{activeTab === 'beneficiaries' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3 bg-[#0d121f]/60 p-3.5 rounded-2xl border border-slate-800/80">
|
||||
<div className="flex items-center gap-2.5 flex-wrap flex-1">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[200px] max-w-sm">
|
||||
<Search className="w-3.5 h-3.5 absolute left-3 top-2.5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search name, location..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 shadow-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="all">All Categories</option>
|
||||
<option value="Youth & Skills">Youth & Skills</option>
|
||||
<option value="Women-Led">Women-Led</option>
|
||||
<option value="SME / Enterprise">SME / Enterprise</option>
|
||||
<option value="Agriculture & Rural">Agriculture & Rural</option>
|
||||
<option value="Healthcare & Community">Healthcare & Community</option>
|
||||
<option value="Education">Education</option>
|
||||
<option value="General">General</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Project Selector */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<FolderKanban className="w-3.5 h-3.5 text-blue-400" />
|
||||
<select
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 max-w-[180px] truncate"
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions: Refresh, Export, Register */}
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
refetchBens();
|
||||
success('Refreshed', 'Beneficiaries register updated from API.');
|
||||
}}
|
||||
disabled={fetchingBens}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold bg-slate-900 text-slate-300 hover:text-white border border-slate-800 hover:bg-slate-800 transition cursor-pointer disabled:opacity-50"
|
||||
title="Force refresh beneficiaries"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${fetchingBens ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
|
||||
<ExportButton
|
||||
data={sortedBens}
|
||||
columns={benExportColumns}
|
||||
filename={`project_beneficiaries_${new Date().toISOString().split('T')[0]}`}
|
||||
title="Beneficiaries Register"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingBen(null);
|
||||
benForm.reset({ name: '', category: 'Youth & Skills', location: 'National', targetCount: 100, reachedCount: 0 });
|
||||
setIsBenModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-500 hover:to-indigo-500 shadow-md shadow-blue-600/25 transition cursor-pointer"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Register Beneficiary</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
@@ -572,11 +592,94 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Compliance Items Table */}
|
||||
{activeTab === 'compliance' && (
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3 bg-[#0d121f]/60 p-3.5 rounded-2xl border border-slate-800/80">
|
||||
<div className="flex items-center gap-2.5 flex-wrap flex-1">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[200px] max-w-sm">
|
||||
<Search className="w-3.5 h-3.5 absolute left-3 top-2.5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search requirement or title..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-800 rounded-xl pl-8 pr-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 shadow-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<select
|
||||
value={compStatusFilter}
|
||||
onChange={(e) => setCompStatusFilter(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="approved">Approved / Satisfied</option>
|
||||
<option value="pending">Pending Review</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Project Selector */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<FolderKanban className="w-3.5 h-3.5 text-blue-400" />
|
||||
<select
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => setSelectedProjectId(e.target.value)}
|
||||
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 max-w-[180px] truncate"
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions: Refresh, Export, Add Compliance Item */}
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
refetchComps();
|
||||
success('Refreshed', 'Compliance items updated from API.');
|
||||
}}
|
||||
disabled={fetchingComps}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold bg-slate-900 text-slate-300 hover:text-white border border-slate-800 hover:bg-slate-800 transition cursor-pointer disabled:opacity-50"
|
||||
title="Force refresh compliance checklist"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${fetchingComps ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
|
||||
<ExportButton
|
||||
data={sortedComps}
|
||||
columns={compExportColumns}
|
||||
filename={`compliance_requirements_${new Date().toISOString().split('T')[0]}`}
|
||||
title="Compliance Requirements"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingComp(null);
|
||||
compForm.reset({ title: '', requirements: '', status: ApprovalStatus.Pending });
|
||||
setIsCompModalOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-slate-800 text-white hover:bg-slate-700 border border-slate-700 shadow-md transition cursor-pointer"
|
||||
>
|
||||
<CheckSquare className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Add Compliance Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
@@ -642,6 +745,7 @@ export const BeneficiariesPage: React.FC = () => {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user