Magnus Smari Smarason 7c3d65dadf
Some checks failed
Build, Lint, and Deploy Arctic Species Portal / test-and-build (push) Failing after 1m0s
Build, Lint, and Deploy Arctic Species Portal / deploy (push) Has been skipped
Fixed CRUD operations for CITES listings, common names, and IUCN assessments. Added admin routes and authentication context. Updated UI components and added new pages for admin functionalities.
2025-05-17 20:58:29 +00:00

303 lines
13 KiB
TypeScript

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Filter, Loader2 } from 'lucide-react';
import { SpeciesDetails, CitesTradeRecord } from '@/lib/api';
import { useQuery } from '@tanstack/react-query';
import { getCitesTradeRecords } from '@/lib/api';
import { useTradeDataVisualization } from '@/hooks/useTradeDataVisualization';
import { useTradeRecordFilters, CountryOption } from '@/hooks/useTradeRecordFilters';
import { getCountryName } from '@/lib/countries';
import { TradeCharts } from '../visualizations/TradeCharts';
import { getTimelineEvents, TimelineEvent } from '@/lib/api';
import { getCatchRecords, CatchRecord } from '@/lib/api';
import { CatchChart } from '../visualizations/CatchChart';
interface TradeDataTabProps {
species: SpeciesDetails;
}
export function TradeDataTab({ species }: TradeDataTabProps) {
const isNarwhal = species.scientific_name === 'Monodon monoceros';
const { data: tradeRecords, isLoading: tradeLoading, error: tradeError } = useQuery({
queryKey: ["tradeRecords", species.id],
queryFn: () => getCitesTradeRecords(species.id),
});
const { data: timelineEvents } = useQuery<TimelineEvent[], Error>({
queryKey: ["timelineEvents", species.id],
queryFn: () => getTimelineEvents(species.id),
});
const { data: catchRecords, isLoading: catchLoading, error: catchError } = useQuery<CatchRecord[], Error>({
queryKey: ["catchRecords", species.id],
queryFn: () => getCatchRecords(species.id),
enabled: isNarwhal,
});
const {
startYearFilter,
setStartYearFilter,
endYearFilter,
setEndYearFilter,
termFilter,
setTermFilter,
importerFilter,
setImporterFilter,
exporterFilter,
setExporterFilter,
filteredRecords,
resetFilters,
yearRange,
uniqueImporters,
uniqueExporters
} = useTradeRecordFilters(tradeRecords);
const {
visualizationData,
years,
terms,
PURPOSE_DESCRIPTIONS,
SOURCE_DESCRIPTIONS
} = useTradeDataVisualization(tradeRecords, filteredRecords);
return (
<Card>
<CardHeader>
<CardTitle>CITES Trade Records</CardTitle>
<CardDescription>International trade data reported to CITES</CardDescription>
</CardHeader>
<CardContent>
{tradeLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : tradeError ? (
<div className="rounded-md bg-red-50 p-4">
<p className="text-red-800">Error loading trade records. Please try again later.</p>
<p className="mt-2 text-sm text-red-700">{(tradeError as Error)?.message || 'Unknown error'}</p>
</div>
) : tradeRecords && tradeRecords.length > 0 ? (
<div className="space-y-6">
<div className="rounded-md border p-4">
<div className="mb-2 flex items-center">
<Filter className="mr-2 h-5 w-5" />
<h3 className="text-base font-medium">Filter Trade Records</h3>
</div>
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-5">
<div className="space-y-2">
<Label htmlFor="start-year-filter">Start Year</Label>
<Select
value={startYearFilter}
onValueChange={setStartYearFilter}
>
<SelectTrigger id="start-year-filter">
<SelectValue placeholder="From Year" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Years</SelectItem>
{years.map(year => (
<SelectItem key={`start-${year}`} value={String(year)}>
{String(year)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="end-year-filter">End Year</Label>
<Select
value={endYearFilter}
onValueChange={setEndYearFilter}
>
<SelectTrigger id="end-year-filter">
<SelectValue placeholder="To Year" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Years</SelectItem>
{years.map(year => (
<SelectItem key={`end-${year}`} value={String(year)}>
{String(year)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="term-filter">Term</Label>
<Select
value={termFilter}
onValueChange={setTermFilter}
>
<SelectTrigger id="term-filter">
<SelectValue placeholder="All Terms" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Terms</SelectItem>
{terms.map(term => (
<SelectItem key={String(term)} value={String(term)}>
{String(term)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="importer-filter">Importer</Label>
<Select
value={importerFilter}
onValueChange={setImporterFilter}
>
<SelectTrigger id="importer-filter">
<SelectValue placeholder="All Importers">
{importerFilter === 'all' ? 'All Importers' : getCountryName(importerFilter)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Importers</SelectItem>
{uniqueImporters.map((country: CountryOption) => (
<SelectItem key={`importer-${country.code}`} value={country.code}>
{country.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="exporter-filter">Exporter</Label>
<Select
value={exporterFilter}
onValueChange={setExporterFilter}
>
<SelectTrigger id="exporter-filter">
<SelectValue placeholder="All Exporters">
{exporterFilter === 'all' ? 'All Exporters' : getCountryName(exporterFilter)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Exporters</SelectItem>
{uniqueExporters.map((country: CountryOption) => (
<SelectItem key={`exporter-${country.code}`} value={country.code}>
{country.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{(startYearFilter !== "all" || endYearFilter !== "all" || termFilter !== "all" || importerFilter !== "all" || exporterFilter !== "all") && (
<div className="mt-4 flex justify-end">
<Button
variant="outline"
size="sm"
onClick={resetFilters}
>
Reset Filters
</Button>
</div>
)}
</div>
<div className="rounded-md bg-muted p-4">
<h3 className="mb-2 text-xl font-semibold">Trade Summary</h3>
<div className="grid gap-4 md:grid-cols-3">
<div className="rounded-md bg-background p-3 shadow-sm">
<h4 className="text-base font-medium text-muted-foreground">Records</h4>
<p className="text-2xl font-bold">{filteredRecords.length}</p>
{filteredRecords.length !== tradeRecords.length && (
<p className="text-sm text-muted-foreground">
of {tradeRecords.length} total
</p>
)}
</div>
<div className="rounded-md bg-background p-3 shadow-sm">
<h4 className="text-base font-medium text-muted-foreground">Year Range</h4>
<p className="text-2xl font-bold">
{filteredRecords.length > 0
? `${startYearFilter === "all" ? yearRange.min : startYearFilter} - ${endYearFilter === "all" ? yearRange.max : endYearFilter}`
: 'N/A'
}
</p>
</div>
<div className="rounded-md bg-background p-3 shadow-sm">
<h4 className="text-base font-medium text-muted-foreground">Total Quantity</h4>
<p className="text-2xl font-bold">
{filteredRecords
.reduce((sum: number, record: CitesTradeRecord) => sum + (Number(record.quantity) || 0), 0)
.toLocaleString()}
</p>
</div>
</div>
</div>
{visualizationData && (
<TradeCharts
visualizationData={visualizationData}
PURPOSE_DESCRIPTIONS={PURPOSE_DESCRIPTIONS}
SOURCE_DESCRIPTIONS={SOURCE_DESCRIPTIONS}
timelineEvents={timelineEvents}
/>
)}
{isNarwhal && catchRecords && catchRecords.length > 0 && (
<div className="mt-8">
<CatchChart catchRecords={catchRecords} />
</div>
)}
{isNarwhal && catchLoading && (
<div className="mt-8 flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="ml-2">Loading catch data...</p>
</div>
)}
{isNarwhal && catchError && (
<div className="mt-8 rounded-md bg-red-50 p-4">
<p className="text-red-800">Error loading catch records.</p>
<p className="mt-2 text-sm text-red-700">{catchError.message || 'Unknown error'}</p>
</div>
)}
{isNarwhal && !catchLoading && !catchError && catchRecords?.length === 0 && (
<div className="mt-8 rounded-md border border-dashed p-8 text-center">
<p className="text-muted-foreground">No specific catch data found for Narwhal.</p>
</div>
)}
<div className="rounded-md bg-blue-50 p-4 text-base mt-8">
<h3 className="mb-2 font-medium text-blue-700">About CITES Trade Data</h3>
<p className="text-blue-700">
The CITES Trade Database, managed by UNEP-WCMC on behalf of the CITES Secretariat, contains records of trade in wildlife
listed in the CITES Appendices. Records include information on species, appendix, purpose, source, quantities, units,
and countries involved in the trade.
</p>
</div>
</div>
) : (
<div className="space-y-4">
<p className="text-muted-foreground">No trade records available for this species.</p>
<div className="rounded-md bg-blue-50 p-4 text-base">
<h3 className="mb-2 font-medium text-blue-700">About CITES Trade Records</h3>
<p className="text-blue-700">
CITES trade records document international trade in wildlife listed in the CITES Appendices.
Not all species have recorded trade data, particularly if they haven't been traded internationally
or if trade reports haven't been submitted to the CITES Trade Database.
</p>
<p className="mt-2 text-blue-700">
If you believe this species should have trade records, please check the following:
</p>
<ul className="list-inside list-disc mt-1 text-blue-700">
<li>Verify the species is listed in a CITES Appendix</li>
<li>Check if the scientific name has been recorded differently</li>
<li>Trade may be recorded at a higher taxonomic level (family or genus)</li>
</ul>
</div>
</div>
)}
</CardContent>
</Card>
);
}