File size: 5,973 Bytes
9d2d895 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | import { getRpcBaseUrl } from '@/services/rpc-client';
import type { GetDisplacementSummaryResponse as ProtoResponse, CountryDisplacement as ProtoCountry, DisplacementFlow as ProtoFlow } from '@/generated/client/worldmonitor/displacement/v1/service_client';
import { createCircuitBreaker, getCSSColor } from '@/utils';
import { DisplacementServiceClient } from '@/services/generated-rpc-clients';
import { publicRpcFetch } from '@/services/public-rpc-fetch';
// βββ Consumer-friendly types (matching legacy shape exactly) βββ
export interface DisplacementFlow {
originCode: string;
originName: string;
asylumCode: string;
asylumName: string;
refugees: number; // number, NOT string
originLat?: number; // flat, NOT GeoCoordinates
originLon?: number;
asylumLat?: number;
asylumLon?: number;
}
export interface CountryDisplacement {
code: string;
name: string;
refugees: number;
asylumSeekers: number;
idps: number;
stateless: number;
totalDisplaced: number;
hostRefugees: number;
hostAsylumSeekers: number;
hostTotal: number;
lat?: number;
lon?: number;
}
export interface UnhcrSummary {
year: number;
globalTotals: {
refugees: number;
asylumSeekers: number;
idps: number;
stateless: number;
total: number;
};
countries: CountryDisplacement[];
topFlows: DisplacementFlow[];
}
export interface UnhcrFetchResult {
ok: boolean;
data: UnhcrSummary;
cachedAt?: string;
}
// βββ Internal: proto -> legacy mapping βββ
const emptyResult: UnhcrSummary = {
year: new Date().getFullYear(),
globalTotals: { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 },
countries: [],
topFlows: [],
};
function toDisplaySummary(proto: ProtoResponse): UnhcrSummary {
const s = proto.summary;
if (!s) return { ...emptyResult, globalTotals: { ...emptyResult.globalTotals } };
const gt = s.globalTotals || { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 };
return {
year: s.year || new Date().getFullYear(),
globalTotals: {
refugees: Number(gt.refugees || 0),
asylumSeekers: Number(gt.asylumSeekers || 0),
idps: Number(gt.idps || 0),
stateless: Number(gt.stateless || 0),
total: Number(gt.total || 0),
},
countries: (s.countries || []).map(toDisplayCountry),
topFlows: (s.topFlows || []).map(toDisplayFlow),
};
}
function toDisplayCountry(proto: ProtoCountry): CountryDisplacement {
return {
code: proto.code || '',
name: proto.name || '',
refugees: Number(proto.refugees || 0),
asylumSeekers: Number(proto.asylumSeekers || 0),
idps: Number(proto.idps || 0),
stateless: Number(proto.stateless || 0),
totalDisplaced: Number(proto.totalDisplaced || 0),
hostRefugees: Number(proto.hostRefugees || 0),
hostAsylumSeekers: Number(proto.hostAsylumSeekers || 0),
hostTotal: Number(proto.hostTotal || 0),
lat: proto.location?.latitude,
lon: proto.location?.longitude,
};
}
function toDisplayFlow(proto: ProtoFlow): DisplacementFlow {
return {
originCode: proto.originCode || '',
originName: proto.originName || '',
asylumCode: proto.asylumCode || '',
asylumName: proto.asylumName || '',
refugees: Number(proto.refugees || 0),
originLat: proto.originLocation?.latitude,
originLon: proto.originLocation?.longitude,
asylumLat: proto.asylumLocation?.latitude,
asylumLon: proto.asylumLocation?.longitude,
};
}
// βββ Client + circuit breaker βββ
async function fetchPublicDisplacementSummary(): Promise<ProtoResponse> {
return new DisplacementServiceClient(getRpcBaseUrl(), { fetch: publicRpcFetch })
.getDisplacementSummary({
year: 0, // 0 = handler uses year fallback
countryLimit: 0, // 0 = all countries
flowLimit: 50, // top 50 flows (matching legacy)
});
}
const breaker = createCircuitBreaker<UnhcrSummary>({
name: 'UNHCR Displacement',
cacheTtlMs: 10 * 60 * 1000,
persistCache: true,
});
// βββ Main fetch (public API) βββ
export async function fetchUnhcrPopulation(): Promise<UnhcrFetchResult> {
const data = await breaker.execute(async () => {
const response = await fetchPublicDisplacementSummary();
return toDisplaySummary(response);
}, emptyResult, { shouldCache: (r) => r.countries.length > 0 });
return {
ok: data !== emptyResult && data.countries.length > 0,
data,
};
}
// βββ Presentation helpers (copied verbatim from legacy src/services/unhcr.ts) βββ
export function getDisplacementColor(totalDisplaced: number): [number, number, number, number] {
if (totalDisplaced >= 1_000_000) return [255, 50, 50, 200];
if (totalDisplaced >= 500_000) return [255, 150, 0, 200];
if (totalDisplaced >= 100_000) return [255, 220, 0, 180];
return [100, 200, 100, 150];
}
export function getDisplacementBadge(totalDisplaced: number): { label: string; color: string } {
if (totalDisplaced >= 1_000_000) return { label: 'CRISIS', color: getCSSColor('--semantic-critical') };
if (totalDisplaced >= 500_000) return { label: 'HIGH', color: getCSSColor('--semantic-high') };
if (totalDisplaced >= 100_000) return { label: 'ELEVATED', color: getCSSColor('--semantic-elevated') };
return { label: '', color: '' };
}
export function formatPopulation(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
return String(n);
}
export function getOriginCountries(data: UnhcrSummary): CountryDisplacement[] {
return [...data.countries]
.filter(c => c.refugees + c.asylumSeekers > 0)
.sort((a, b) => (b.refugees + b.asylumSeekers) - (a.refugees + a.asylumSeekers));
}
export function getHostCountries(data: UnhcrSummary): CountryDisplacement[] {
return [...data.countries]
.filter(c => (c.hostTotal || 0) > 0)
.sort((a, b) => (b.hostTotal || 0) - (a.hostTotal || 0));
}
|