303 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|