mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
359 lines
13 KiB
TypeScript
359 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';
|
|
|
|
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: any) => {
|
|
// 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: any, b: any) => {
|
|
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: any) => org.Sector).filter(Boolean))
|
|
);
|
|
const subtypes = Array.from(
|
|
new Set(organizations.map((org: any) => 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: any) => {
|
|
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: any) => (
|
|
<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;
|