feat: nature des échanges — catégorisation et détail des commentaires de transactions
ci/woodpecker/push/woodpecker Pipeline was successful

- Nouveau commentParser.ts : ~80 règles regex multilingues, 11 catégories
- SubsquidAdapter : fetch du champ comment.remark depuis SubSquid
- Transaction et TransactionArc : champs comment et category
- StatsPanel : section Nature des échanges avec barres cliquables (détail inline)
- FlowMap : tooltip au survol des arcs avec répartition catégories + commentaires
- InfoPanel mis à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-04-21 21:29:59 +02:00
parent 6b7591db32
commit 8e396cd331
10 changed files with 562 additions and 27 deletions
+25 -8
View File
@@ -1,4 +1,6 @@
import type { Transaction } from './mockData';
import type { TxCategory } from './commentParser';
import { aggregateCategories } from './commentParser';
export interface TransactionArc {
id: string;
@@ -14,6 +16,8 @@ export interface TransactionArc {
toCity: string;
toCountry: string;
toKey: string;
comment: string | null;
category: TxCategory;
}
/** Corridor agrégé par paire de villes (fromCity → toCity). */
@@ -28,6 +32,8 @@ export interface Corridor {
toCountry: string;
totalVolume: number;
count: number;
categories: { category: TxCategory; count: number; volume: number }[];
comments: string[]; // échantillon de commentaires bruts (max 5, non nuls)
}
export interface FlowStats {
@@ -40,21 +46,30 @@ export interface FlowStats {
/** Agrège les arcs individuels en corridors ville→ville, triés par volume. */
export function buildCorridors(arcs: TransactionArc[]): Corridor[] {
const map = new Map<string, Corridor>();
const map = new Map<string, { corridor: Omit<Corridor, 'categories' | 'comments'>; items: TransactionArc[] }>();
for (const arc of arcs) {
const key = `${arc.fromCity}||${arc.toCity}`;
if (!map.has(key)) {
map.set(key, {
fromCity: arc.fromCity, fromLat: arc.fromLat, fromLng: arc.fromLng, fromCountry: arc.fromCountry,
toCity: arc.toCity, toLat: arc.toLat, toLng: arc.toLng, toCountry: arc.toCountry,
totalVolume: 0, count: 0,
corridor: {
fromCity: arc.fromCity, fromLat: arc.fromLat, fromLng: arc.fromLng, fromCountry: arc.fromCountry,
toCity: arc.toCity, toLat: arc.toLat, toLng: arc.toLng, toCountry: arc.toCountry,
totalVolume: 0, count: 0,
},
items: [],
});
}
const c = map.get(key)!;
c.totalVolume += arc.amount;
c.count++;
const entry = map.get(key)!;
entry.corridor.totalVolume += arc.amount;
entry.corridor.count++;
entry.items.push(arc);
}
return [...map.values()].sort((a, b) => b.totalVolume - a.totalVolume);
return [...map.values()].map(({ corridor, items }) => ({
...corridor,
categories: aggregateCategories(items.map((a) => ({ category: a.category, amount: a.amount }))),
comments: items.map((a) => a.comment).filter((c): c is string => !!c).slice(0, 5),
})).sort((a, b) => b.totalVolume - a.totalVolume);
}
export function computeFlowStats(arcs: TransactionArc[]): FlowStats {
@@ -114,6 +129,8 @@ export function buildMockArcs(transactions: Transaction[]): TransactionArc[] {
toLat: to.lat, toLng: to.lng,
toCity: to.city, toCountry: to.countryCode,
toKey: to.toKey,
comment: from.comment,
category: from.category,
});
}
return arcs;