turash/bugulma/frontend/components/admin/OrganizationTable.tsx

216 lines
8.4 KiB
TypeScript

import Badge from '@/components/ui/Badge.tsx';
import Button from '@/components/ui/Button.tsx';
import Input from '@/components/ui/Input.tsx';
import VerifiedBadge from '@/components/ui/VerifiedBadge.tsx';
import { getSectorDisplay } from '@/constants.tsx';
import { useOrganizationTable } from '@/hooks/features/useOrganizationTable.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import {
getTranslatedSectorName
} from '@/lib/sector-mapper.ts';
import { getOrganizationSubtypeLabel } from '@/schemas/organizationSubtype.ts';
import { Organization } from '@/types.ts';
import { useVerifyOrganization, useRejectVerification } from '@/hooks/api/useAdminAPI.ts';
import React, { useCallback } from 'react';
interface OrganizationTableProps {
onUpdateOrganization: (org: Organization) => void;
}
const VerifyButton = React.memo(
({ org, onVerify }: { org: Organization; onVerify: (org: Organization) => void }) => {
const { t } = useTranslation();
const handleClick = useCallback(() => {
onVerify(org);
}, [onVerify, org]);
return (
<Button variant="ghost" size="sm" onClick={handleClick}>
{org.Verified ? t('adminPage.orgTable.unverify') : t('adminPage.orgTable.verify')}
</Button>
);
}
);
VerifyButton.displayName = 'VerifyButton';
const OrganizationTable = ({ onUpdateOrganization }: OrganizationTableProps) => {
const { t } = useTranslation();
const { filter, setFilter, searchTerm, setSearchTerm, filteredOrgs } = useOrganizationTable();
const { mutate: verifyOrganization } = useVerifyOrganization();
const { mutate: rejectVerification } = useRejectVerification();
const handleVerify = useCallback(
(org: Organization) => {
if (org.Verified) {
// If already verified, we could reject or just update locally
// For now, just update locally
onUpdateOrganization({ ...org, Verified: false });
} else {
verifyOrganization(
{ id: org.ID, notes: 'Verified via admin panel' },
{
onSuccess: () => {
onUpdateOrganization({ ...org, Verified: true });
},
onError: (error) => {
console.error('Failed to verify organization:', error);
},
}
);
}
},
[onUpdateOrganization, verifyOrganization]
);
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-4">
<Input
placeholder={t('adminPage.orgTable.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-xs"
/>
<div className="flex items-center gap-2">
<Button
variant={filter === 'all' ? 'primary' : 'outline'}
onClick={() => setFilter('all')}
>
{t('adminPage.orgTable.filters.all')}
</Button>
<Button
variant={filter === 'verified' ? 'primary' : 'outline'}
onClick={() => setFilter('verified')}
>
{t('adminPage.orgTable.filters.verified')}
</Button>
<Button
variant={filter === 'unverified' ? 'primary' : 'outline'}
onClick={() => setFilter('unverified')}
>
{t('adminPage.orgTable.filters.unverified')}
</Button>
</div>
</div>
<div className="border rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted/50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.logo')}
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.name')}
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.sector')}
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.type')}
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.needsOffers')}
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider"
>
{t('adminPage.orgTable.status')}
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">{t('adminPage.orgTable.action')}</span>
</th>
</tr>
</thead>
<tbody className="bg-background divide-y divide-border">
{filteredOrgs.map((org) => {
// Get sector display information
const orgSector = org.Sector || '';
const sectorDisplay = getSectorDisplay(orgSector);
return (
<tr key={org.ID}>
<td className="px-6 py-4">
<div className="h-10 w-10 rounded-md flex items-center justify-center shrink-0 bg-muted overflow-hidden border">
{org.LogoURL ? (
<img
src={org.LogoURL}
alt={`${org.Name} logo`}
className="w-full h-full object-cover"
loading="lazy"
decoding="async"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
) : (
<div
className={`w-full h-full flex items-center justify-center bg-sector-${sectorDisplay.colorKey}`}
>
{React.cloneElement(sectorDisplay.icon, {
className: 'h-5 w-5 text-primary-foreground',
})}
</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{org.Name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{getTranslatedSectorName(orgSector, t)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
{org.Subtype ? (
<Badge variant="secondary" className="text-xs">
{getOrganizationSubtypeLabel(org.Subtype)}
</Badge>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
<div title="Resource flow data will be available when backend API supports counting organization resources">
{t('adminPage.orgTable.notAvailable')}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{org.Verified ? (
<VerifiedBadge />
) : (
<Badge
variant="outline"
className="border-destructive/30 bg-destructive/10 text-destructive"
>
{t('adminPage.orgTable.unverified')}
</Badge>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<VerifyButton org={org} onVerify={handleVerify} />
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
export default React.memo(OrganizationTable);