turash/bugulma/frontend/pages/OrganizationsListPage.tsx
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

360 lines
13 KiB
TypeScript

import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Briefcase, Filter, Grid3X3, List, Plus } from 'lucide-react';
import { MainLayout } from '@/components/layout/MainLayout.tsx';
import PageHeader from '@/components/layout/PageHeader.tsx';
import OrganizationCard from '@/components/organization/OrganizationCard.tsx';
import Badge from '@/components/ui/Badge.tsx';
import Button from '@/components/ui/Button.tsx';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card.tsx';
import { Container, Flex, Grid, Stack } from '@/components/ui/layout';
import SearchInput from '@/components/ui/SearchInput.tsx';
import Select from '@/components/ui/Select.tsx';
import Spinner from '@/components/ui/Spinner.tsx';
import { useOrganizations } from '@/hooks/api/useOrganizationsAPI.ts';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { useNavigation } from '@/hooks/useNavigation.tsx';
import { getTranslatedSectorName } from '@/lib/sector-mapper.ts';
import { getOrganizationSubtypeLabel } from '@/schemas/organizationSubtype.ts';
import type { Organization } from '@/types.ts';
const OrganizationsListPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { handleBackNavigation, handleFooterNavigate } = useNavigation();
const { data: organizations, isLoading, error } = useOrganizations();
// Filter and search state
const [searchQuery, setSearchQuery] = useState('');
const [selectedSector, setSelectedSector] = useState<string>('all');
const [selectedSubtype, setSelectedSubtype] = useState<string>('all');
const [sortBy, setSortBy] = useState<string>('name');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showVerifiedOnly, setShowVerifiedOnly] = useState(false);
// Process organizations data
const processedOrganizations = useMemo(() => {
if (!organizations) return [];
const filtered = organizations.filter((org: Organization) => {
// Search filter
const matchesSearch =
!searchQuery ||
org.Name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
org.Description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
org.Address?.toLowerCase().includes(searchQuery.toLowerCase());
// Sector filter
const matchesSector = selectedSector === 'all' || org.Sector === selectedSector;
// Subtype filter
const matchesSubtype = selectedSubtype === 'all' || org.Subtype === selectedSubtype;
// Verified filter
const matchesVerified = !showVerifiedOnly || org.Verified;
return matchesSearch && matchesSector && matchesSubtype && matchesVerified;
});
// Sort
filtered.sort((a: Organization, b: Organization) => {
switch (sortBy) {
case 'name':
return a.Name?.localeCompare(b.Name || '') || 0;
case 'sector':
return (a.Sector || '').localeCompare(b.Sector || '');
case 'verified':
return (b.Verified ? 1 : 0) - (a.Verified ? 1 : 0);
case 'activity':
// Sort by number of resource flows + matches
const aActivity = (a.ResourceFlows?.length || 0) + (a.Matches?.length || 0);
const bActivity = (b.ResourceFlows?.length || 0) + (b.Matches?.length || 0);
return bActivity - aActivity;
default:
return 0;
}
});
return filtered;
}, [organizations, searchQuery, selectedSector, selectedSubtype, sortBy, showVerifiedOnly]);
// Get unique sectors and subtypes for filters
const filterOptions = useMemo(() => {
if (!organizations) return { sectors: [], subtypes: [] };
const sectors = Array.from(
new Set(organizations.map((org: Organization) => org.Sector).filter(Boolean))
);
const subtypes = Array.from(
new Set(organizations.map((org: Organization) => org.Subtype).filter(Boolean))
);
return { sectors, subtypes };
}, [organizations]);
// Sector options for dropdown
const sectorOptions = [
{ value: 'all', label: t('organizationsList.allSectors') },
...filterOptions.sectors.map((sector) => ({
value: sector,
label: getTranslatedSectorName(sector),
})),
];
// Subtype options for dropdown
const subtypeOptions = [
{ value: 'all', label: t('organizationsList.allSubtypes') },
...filterOptions.subtypes.map((subtype) => ({
value: subtype,
label: getOrganizationSubtypeLabel(subtype),
})),
];
// Sort options
const sortOptions = [
{ value: 'name', label: t('organizationsList.sortByName') },
{ value: 'sector', label: t('organizationsList.sortBySector') },
{ value: 'verified', label: t('organizationsList.sortByVerified') },
{ value: 'activity', label: t('organizationsList.sortByActivity') },
];
const handleCreateOrganization = () => {
navigate('/organizations/new');
};
const handleOrganizationClick = (organization: Organization) => {
navigate(`/organization/${organization.ID}`);
};
const clearFilters = () => {
setSearchQuery('');
setSelectedSector('all');
setSelectedSubtype('all');
setShowVerifiedOnly(false);
setSortBy('name');
};
const activeFiltersCount = [
searchQuery,
selectedSector !== 'all',
selectedSubtype !== 'all',
showVerifiedOnly,
].filter(Boolean).length;
if (isLoading) {
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<div className="flex items-center justify-center min-h-96">
<div className="text-center">
<Spinner className="h-8 w-8 mx-auto mb-4" />
<p className="text-muted-foreground">{t('organizationsList.loading')}</p>
</div>
</div>
</Container>
</MainLayout>
);
}
if (error) {
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('organizationsList.errorTitle')}
subtitle={t('organizationsList.errorSubtitle')}
onBack={handleBackNavigation}
/>
<Card>
<CardContent className="py-12">
<div className="text-center">
<p className="text-destructive mb-4">{error.message}</p>
<Button onClick={() => window.location.reload()}>{t('common.retry')}</Button>
</div>
</CardContent>
</Card>
</Container>
</MainLayout>
);
}
return (
<MainLayout onNavigate={handleFooterNavigate} className="bg-muted/30">
<Container size="2xl" className="py-8 sm:py-12">
<PageHeader
title={t('organizationsList.title')}
subtitle={t('organizationsList.subtitle')}
onBack={handleBackNavigation}
/>
<Stack spacing="2xl">
{/* Header Actions */}
<Flex align="center" justify="between" className="flex-wrap gap-4">
<div className="flex items-center gap-4">
<Button onClick={handleCreateOrganization}>
<Plus className="h-4 mr-2 text-current w-4" />
{t('organizationsList.createOrganization')}
</Button>
{activeFiltersCount > 0 && (
<Button variant="outline" onClick={clearFilters}>
{t('organizationsList.clearFilters')} ({activeFiltersCount})
</Button>
)}
</div>
<Flex gap="sm">
<Button
variant={viewMode === 'grid' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('grid')}
>
<Grid3X3 className="h-4 text-current w-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('list')}
>
<List className="h-4 text-current w-4" />
</Button>
</Flex>
</Flex>
{/* Search and Filters */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Filter className="h-4 h-5 text-current w-4 w-5" />
{t('organizationsList.searchAndFilters')}
</CardTitle>
</CardHeader>
<CardContent>
<Grid cols={{ md: 2, lg: 4 }} gap="md">
<div>
<label className="block text-sm font-medium mb-2">
{t('organizationsList.search')}
</label>
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
placeholder={t('organizationsList.searchPlaceholder')}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('organizationsList.sector')}
</label>
<Select
value={selectedSector}
onValueChange={setSelectedSector}
options={sectorOptions}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('organizationsList.subtype')}
</label>
<Select
value={selectedSubtype}
onValueChange={setSelectedSubtype}
options={subtypeOptions}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t('organizationsList.sortBy')}
</label>
<Select value={sortBy} onValueChange={setSortBy} options={sortOptions} />
</div>
</Grid>
{/* Additional filters */}
<div className="flex items-center gap-4 mt-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={showVerifiedOnly}
onChange={(e) => setShowVerifiedOnly(e.target.checked)}
className="rounded"
/>
<span className="text-sm">{t('organizationsList.verifiedOnly')}</span>
</label>
</div>
</CardContent>
</Card>
{/* Results Summary */}
<div className="flex items-center justify-between">
<p className="text-muted-foreground">
{t('organizationsList.showingResults', {
shown: processedOrganizations.length,
total: organizations?.length || 0,
})}
</p>
{activeFiltersCount > 0 && (
<Badge variant="secondary">
{activeFiltersCount} {t('organizationsList.filtersActive')}
</Badge>
)}
</div>
{/* Organizations Grid/List */}
{processedOrganizations.length > 0 ? (
<Grid cols={viewMode === 'grid' ? { sm: 1, md: 2, lg: 3 } : { cols: 1 }} gap="md">
{processedOrganizations.map((organization: Organization) => (
<OrganizationCard
key={organization.ID}
organization={organization}
showDetails={viewMode === 'grid'}
onClick={handleOrganizationClick}
/>
))}
</Grid>
) : (
<Card>
<CardContent className="py-12">
<div className="text-center">
<Briefcase className="h-12 h-4 mb-4 mx-auto text-current text-muted-foreground w-12 w-4" />
<h3 className="text-lg font-semibold mb-2">
{t('organizationsList.noOrganizations')}
</h3>
<p className="text-muted-foreground mb-4">
{searchQuery ||
selectedSector !== 'all' ||
selectedSubtype !== 'all' ||
showVerifiedOnly
? t('organizationsList.noOrganizationsMatchFilters')
: t('organizationsList.noOrganizationsYet')}
</p>
{(searchQuery ||
selectedSector !== 'all' ||
selectedSubtype !== 'all' ||
showVerifiedOnly) && (
<Button onClick={clearFilters}>{t('organizationsList.clearFilters')}</Button>
)}
{!searchQuery &&
selectedSector === 'all' &&
selectedSubtype === 'all' &&
!showVerifiedOnly && (
<Button onClick={handleCreateOrganization}>
<Plus className="h-4 mr-2 text-current w-4" />
{t('organizationsList.createFirstOrganization')}
</Button>
)}
</div>
</CardContent>
</Card>
)}
</Stack>
</Container>
</MainLayout>
);
};
export default OrganizationsListPage;