mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
337 lines
12 KiB
TypeScript
337 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;
|