feat: nature des échanges — catégorisation et détail des commentaires de transactions
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -1,9 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { PeriodStats } from '../services/DataService';
|
||||
import type { FlowStats } from '../data/arcData';
|
||||
import { Sparkline } from './Sparkline';
|
||||
import { ServiceStatusDots } from './ServiceStatusDots';
|
||||
import { useServiceStatus } from '../hooks/useServiceStatus';
|
||||
import { CATEGORY_LABELS, CATEGORY_COLORS, type TxCategory } from '../data/commentParser';
|
||||
import type { Transaction } from '../data/mockData';
|
||||
|
||||
interface StatsPanelProps {
|
||||
stats: PeriodStats | null;
|
||||
@@ -19,6 +21,7 @@ interface StatsPanelProps {
|
||||
onClose?: () => void;
|
||||
onEndpointChange?: () => void;
|
||||
allTimestamps?: number[];
|
||||
transactions?: Transaction[];
|
||||
}
|
||||
|
||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||
@@ -65,8 +68,9 @@ function CityRow({ city, volume, count, countryCode, accent }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose, onEndpointChange, className, allTimestamps = [] }: StatsPanelProps) {
|
||||
export function StatsPanel({ stats, loading, periodDays, source, currentUD, animationLabel, viewMode = 'heatmap', flowStats, focusCity, onClose, onEndpointChange, className, allTimestamps = [], transactions = [] }: StatsPanelProps) {
|
||||
const { subsquid, cesium, recheck } = useServiceStatus();
|
||||
const [openCategory, setOpenCategory] = useState<TxCategory | null>(null);
|
||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||
const prevStats = useRef<PeriodStats | null>(null);
|
||||
|
||||
@@ -309,6 +313,73 @@ export function StatsPanel({ stats, loading, periodDays, source, currentUD, anim
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Nature des échanges */}
|
||||
{!loading && stats && stats.categoryBreakdown.length > 0 && stats.commentedCount > 0 && (
|
||||
<div className="space-y-2 border-t border-[#1e1f2a] pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#4b5563] text-xs uppercase tracking-widest">Nature des échanges</p>
|
||||
<p className="text-[#2e2f3a] text-[10px] font-mono">{stats.commentedCount} commentés</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{stats.categoryBreakdown
|
||||
.filter((c) => c.category !== 'migration' && c.category !== 'ticket')
|
||||
.slice(0, 7)
|
||||
.map((c) => {
|
||||
const pct = Math.round((c.count / stats.commentedCount) * 100);
|
||||
const isOpen = openCategory === c.category;
|
||||
const detail = transactions.filter((t) => t.category === c.category && t.comment);
|
||||
return (
|
||||
<div key={c.category}>
|
||||
<button
|
||||
onClick={() => setOpenCategory(isOpen ? null : c.category)}
|
||||
className="w-full text-left group"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className={`text-[11px] transition-colors ${isOpen ? 'text-white' : 'text-[#9ca3af] group-hover:text-white'}`}>
|
||||
{isOpen ? '▾' : '▸'} {CATEGORY_LABELS[c.category]}
|
||||
</span>
|
||||
<span className="text-[#4b5563] text-[10px] font-mono">{c.count} · {pct}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#1e1f2a] rounded-full h-1">
|
||||
<div
|
||||
className="h-1 rounded-full transition-all duration-500"
|
||||
style={{ width: `${pct}%`, backgroundColor: CATEGORY_COLORS[c.category] }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="mt-1.5 mb-1 bg-[#0a0b0f] border border-[#1e1f2a] rounded-lg overflow-hidden">
|
||||
{detail.length === 0 ? (
|
||||
<p className="text-[#4b5563] text-[10px] px-3 py-2">Aucun commentaire disponible.</p>
|
||||
) : (
|
||||
<div className="max-h-48 overflow-y-auto divide-y divide-[#1e1f2a]">
|
||||
{detail.slice(0, 30).map((t) => (
|
||||
<div key={t.id} className="px-3 py-1.5 flex items-start justify-between gap-2">
|
||||
<p className="text-[#9ca3af] text-[10px] italic leading-snug flex-1 min-w-0 truncate">
|
||||
"{t.comment}"
|
||||
</p>
|
||||
<span className="text-[#d4a843] text-[10px] font-mono shrink-0">
|
||||
{t.amount.toLocaleString('fr-FR', { maximumFractionDigits: 1 })} Ğ1
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{detail.length > 30 && (
|
||||
<p className="text-[#4b5563] text-[10px] px-3 py-1.5 text-center">
|
||||
+{detail.length - 30} autres
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto pt-4 border-t border-[#1e1f2a] space-y-1.5">
|
||||
<p className="text-[#2e2f3a] text-xs text-center">
|
||||
|
||||
Reference in New Issue
Block a user