turash/bugulma/frontend/pages/admin/AdminOrganizationsPage.tsx

342 lines
12 KiB
TypeScript

import { Button } from '@/components/ui';
import { DataTable } from '@/components/admin/DataTable.tsx';
import { SearchAndFilter } from '@/components/admin/SearchAndFilter.tsx';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useOrganizations } from '@/hooks/useOrganizations.ts';
import { Organization } from '@/types.ts';
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Plus, Download, Upload, CheckCircle, XCircle, Edit, Eye } from 'lucide-react';
import { useBulkVerifyOrganizations } from '@/hooks/api/useAdminAPI.ts';
import { useToast } from '@/hooks/useToast.ts';
const AdminOrganizationsPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { organizations, updateOrganization } = useOrganizations();
const { success, error, warning } = useToast();
// Search and filter state
const [searchTerm, setSearchTerm] = useState('');
const [filterValues, setFilterValues] = useState({
sector: '',
type: '',
verification: '',
});
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
// Bulk operations
const { mutate: bulkVerifyOrganizations, isPending: isBulkVerifying } = useBulkVerifyOrganizations();
// Filter organizations
const filteredOrganizations = useMemo(() => {
return organizations.filter((org) => {
const matchesSearch = !searchTerm ||
org.Name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
org.Description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
org.Sector?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesSector = !filterValues.sector || org.Sector === filterValues.sector;
const matchesType = !filterValues.type || org.Type === filterValues.type;
const matchesVerification = !filterValues.verification ||
(filterValues.verification === 'verified' && org.Verified) ||
(filterValues.verification === 'unverified' && !org.Verified) ||
(filterValues.verification === 'pending' && org.PendingVerification);
return matchesSearch && matchesSector && matchesType && matchesVerification;
});
}, [organizations, searchTerm, filterValues]);
// Paginated data
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredOrganizations.slice(startIndex, endIndex);
}, [filteredOrganizations, currentPage, pageSize]);
const totalPages = Math.ceil(filteredOrganizations.length / pageSize);
// Table columns
const columns = [
{
key: 'logo',
header: t('adminPage.orgTable.logo'),
sortable: false,
width: '80px',
render: (org: Organization) => (
<div className="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{org.Logo ? (
<img src={org.Logo} alt={org.Name} className="w-full h-full object-cover" />
) : (
<div className="text-xs text-muted-foreground">{org.Name?.charAt(0).toUpperCase()}</div>
)}
</div>
),
},
{
key: 'name',
header: t('adminPage.orgTable.name'),
sortable: true,
render: (org: Organization) => (
<div>
<div className="font-medium">{org.Name}</div>
<div className="text-sm text-muted-foreground">{org.Description?.slice(0, 50)}...</div>
</div>
),
},
{
key: 'sector',
header: t('adminPage.orgTable.sector'),
sortable: true,
render: (org: Organization) => org.Sector,
},
{
key: 'type',
header: t('adminPage.orgTable.type'),
sortable: true,
render: (org: Organization) => org.Type,
},
{
key: 'needs_offers',
header: t('adminPage.orgTable.needsOffers'),
sortable: false,
render: (org: Organization) => (
<div className="text-sm">
<div className="text-green-600">Needs: {org.Needs?.length || 0}</div>
<div className="text-blue-600">Offers: {org.Offers?.length || 0}</div>
</div>
),
},
{
key: 'status',
header: t('adminPage.orgTable.status'),
sortable: true,
render: (org: Organization) => (
<div className="flex items-center gap-2">
{org.Verified ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
{t('adminPage.orgTable.verified')}
</span>
) : org.PendingVerification ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800">
{t('adminPage.orgTable.pending')}
</span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
<XCircle className="w-3 h-3 mr-1" />
{t('adminPage.orgTable.unverified')}
</span>
)}
</div>
),
},
{
key: 'actions',
header: t('adminPage.orgTable.actions'),
sortable: false,
render: (org: Organization) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/organization/${org.ID}`)}
aria-label="View organization"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/admin/organizations/${org.ID}/edit`)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant={org.Verified ? "secondary" : "default"}
size="sm"
onClick={() => updateOrganization({ ...org, Verified: !org.Verified })}
>
{org.Verified ? t('adminPage.orgTable.unverify') : t('adminPage.orgTable.verify')}
</Button>
</div>
),
},
];
// Bulk actions
const handleBulkVerify = () => {
const selectedIds = Array.from(selectedRows);
if (selectedIds.length === 0) {
warning(t('adminPage.orgTable.noSelection'), {
title: t('adminPage.orgTable.selectOrgsFirst'),
});
return;
}
bulkVerifyOrganizations(selectedIds, {
onSuccess: () => {
success(t('adminPage.orgTable.bulkVerifySuccess'), {
title: t('adminPage.orgTable.bulkVerifySuccessDesc', { count: selectedIds.length }),
});
setSelectedRows(new Set());
},
onError: () => {
error(t('adminPage.orgTable.bulkVerifyError'), {
title: t('adminPage.orgTable.bulkVerifyErrorDesc'),
});
},
});
};
const handleExport = () => {
// Export current filtered view
const csvData = filteredOrganizations.map(org => ({
Name: org.Name,
Sector: org.Sector,
Type: org.Type,
Verified: org.Verified ? 'Yes' : 'No',
CreatedAt: org.CreatedAt,
}));
const csvContent = [
Object.keys(csvData[0]).join(','),
...csvData.map(row => Object.values(row).join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'organizations.csv';
a.click();
window.URL.revokeObjectURL(url);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{t('adminPage.organizations.title')}</h1>
<p className="text-muted-foreground">
{t('adminPage.organizations.subtitle')} ({filteredOrganizations.length} {t('adminPage.organizations.total')})
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
{t('adminPage.organizations.export')}
</Button>
<Button onClick={() => navigate('/admin/organizations/new')}>
<Plus className="w-4 h-4 mr-2" />
{t('adminPage.organizations.newOrg')}
</Button>
</div>
</div>
{/* Search and Filters */}
<SearchAndFilter
search={{
value: searchTerm,
onChange: setSearchTerm,
placeholder: t('adminPage.organizations.searchPlaceholder'),
}}
filters={{
filters: [
{
id: 'sector',
label: t('adminPage.organizations.filterSector'),
type: 'select',
options: [
{ label: t('adminPage.organizations.allSectors'), value: '' },
{ label: 'Technology', value: 'technology' },
{ label: 'Manufacturing', value: 'manufacturing' },
{ label: 'Agriculture', value: 'agriculture' },
// Add more sectors as needed
],
},
{
id: 'type',
label: t('adminPage.organizations.filterType'),
type: 'select',
options: [
{ label: t('adminPage.organizations.allTypes'), value: '' },
{ label: 'Company', value: 'company' },
{ label: 'Non-profit', value: 'nonprofit' },
{ label: 'Government', value: 'government' },
],
},
{
id: 'verification',
label: t('adminPage.organizations.filterVerification'),
type: 'select',
options: [
{ label: t('adminPage.organizations.allStatuses'), value: '' },
{ label: t('adminPage.organizations.verifiedOnly'), value: 'verified' },
{ label: t('adminPage.organizations.unverifiedOnly'), value: 'unverified' },
{ label: t('adminPage.organizations.pendingOnly'), value: 'pending' },
],
},
],
values: filterValues,
onChange: setFilterValues,
}}
/>
{/* Bulk Actions */}
{selectedRows.size > 0 && (
<div className="flex items-center gap-2 p-4 bg-muted rounded-lg">
<span className="text-sm font-medium">
{selectedRows.size} {t('adminPage.organizations.selected')}
</span>
<Button
variant="default"
size="sm"
onClick={handleBulkVerify}
disabled={isBulkVerifying}
>
<CheckCircle className="w-4 h-4 mr-2" />
{t('adminPage.organizations.bulkVerify')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedRows(new Set())}
>
{t('adminPage.organizations.clearSelection')}
</Button>
</div>
)}
{/* Data Table */}
<DataTable
columns={columns}
data={paginatedData}
getRowId={(org) => org.ID}
isLoading={false}
selection={{
selectedRows,
onSelectionChange: setSelectedRows,
}}
pagination={{
currentPage,
totalPages,
pageSize,
totalItems: filteredOrganizations.length,
onPageChange: setCurrentPage,
onPageSizeChange: setPageSize,
}}
emptyMessage={t('adminPage.organizations.noOrganizations')}
emptyDescription={t('adminPage.organizations.noOrganizationsDesc')}
/>
</div>
);
};
export default AdminOrganizationsPage;