interacmanagernew / files /ScanControls.tsx
MichaelEdou
Initial commit — ICC Interac Manager full-stack app
149698e
// packages/web/src/components/scan/ScanControls.tsx
//
// THIS IS THE KEY COMPONENT that makes "Scanner maintenant" actually work.
// It connects the date range selection + scan button to the backend API.
//
// Usage in your DashboardPage or ScanPage:
// import ScanControls from '@/components/scan/ScanControls';
// <ScanControls onScanComplete={(result) => refetchTransactions()} />
import { useState, useCallback } from 'react';
import { useEmailScan, type ScanPreset } from '../../hooks/useEmailScan';
// Import your UI components (adjust paths to your project)
// import { Button } from '@/components/ui/button';
// import { ScanLine, Loader2 } from 'lucide-react';
interface ScanControlsProps {
onScanComplete?: (result: { found: number; parsed: number; skipped: number; errors: number }) => void;
onNewTransaction?: (transaction: any) => void;
}
export default function ScanControls({ onScanComplete, onNewTransaction }: ScanControlsProps) {
// ═══ STATE ═══
const [selectedPreset, setSelectedPreset] = useState<ScanPreset>('last7days');
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
const [forceRescan, setForceRescan] = useState(false);
// ═══ SCAN HOOK — connects to API + WebSocket ═══
const {
isScanning,
progress,
result,
error,
startScan,
progressPercent,
} = useEmailScan();
// Notify parent when scan completes
if (result && onScanComplete) {
onScanComplete(result);
}
// ═══ HANDLERS ═══
const handleScanClick = useCallback(() => {
startScan({
preset: selectedPreset,
startDate: selectedPreset === 'custom' ? customStartDate : undefined,
endDate: selectedPreset === 'custom' ? customEndDate : undefined,
forceRescan,
});
}, [selectedPreset, customStartDate, customEndDate, forceRescan, startScan]);
const presetButtons: { value: ScanPreset; label: string }[] = [
{ value: 'today', label: "Aujourd'hui" },
{ value: 'last7days', label: '7 derniers jours' },
{ value: 'custom', label: 'Période personnalisée' },
];
// ═══ DATE DISPLAY ═══
const getDateRangeDisplay = () => {
const now = new Date();
const fmt = (d: Date) => d.toLocaleDateString('fr-CA', { day: 'numeric', month: 'short' });
switch (selectedPreset) {
case 'today':
return fmt(now);
case 'last7days': {
const start = new Date(now);
start.setDate(start.getDate() - 7);
return `${fmt(start)}${fmt(now)}`;
}
case 'custom':
if (customStartDate && customEndDate) {
return `${fmt(new Date(customStartDate))}${fmt(new Date(customEndDate))}`;
}
return 'Sélectionnez les dates';
}
};
// ═══ RENDER ═══
return (
<div className="flex flex-col gap-4">
{/* PRESET BUTTONS + SCAN BUTTON ROW */}
<div className="flex items-center gap-4 flex-wrap">
{/* Date preset toggle group */}
<div className="flex items-center gap-2 bg-card p-1 rounded-lg border border-border shadow-sm">
{presetButtons.map((btn) => (
<button
key={btn.value}
onClick={() => setSelectedPreset(btn.value)}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
selectedPreset === btn.value
? 'bg-slate-100 text-slate-900 hover:bg-slate-200'
: 'text-slate-500 hover:bg-slate-50 hover:text-slate-900'
}`}
>
{btn.label}
</button>
))}
</div>
{/* Date range display */}
<div className="hidden sm:flex items-center gap-2 bg-card px-3 py-1.5 rounded-lg border border-border shadow-sm text-sm text-slate-600">
<span className="font-medium text-slate-900">{getDateRangeDisplay()}</span>
</div>
{/* Separator */}
<div className="h-8 w-px bg-slate-200 mx-2" />
{/* ════════════════════════════════════════════════ */}
{/* 🔥 THE SCAN BUTTON — this is what triggers it! */}
{/* ════════════════════════════════════════════════ */}
<button
onClick={handleScanClick}
disabled={isScanning || (selectedPreset === 'custom' && (!customStartDate || !customEndDate))}
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium
bg-primary text-primary-foreground hover:bg-primary/90
h-9 px-4 py-2 gap-2
shadow-md shadow-blue-500/20 hover:shadow-blue-500/30
active:scale-95 transition-all
disabled:pointer-events-none disabled:opacity-50"
>
{isScanning ? (
<>
{/* Spinning loader */}
<svg className="animate-spin h-[18px] w-[18px]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Scan en cours...
</>
) : (
<>
{/* Scan icon */}
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 7V5a2 2 0 0 1 2-2h2" />
<path d="M17 3h2a2 2 0 0 1 2 2v2" />
<path d="M21 17v2a2 2 0 0 1-2 2h-2" />
<path d="M7 21H5a2 2 0 0 1-2-2v-2" />
<path d="M7 12h10" />
</svg>
Scanner maintenant
</>
)}
</button>
</div>
{/* CUSTOM DATE PICKERS (only when custom is selected) */}
{selectedPreset === 'custom' && (
<div className="flex items-center gap-3">
<div>
<label className="text-xs text-slate-500 block mb-1">Date de début</label>
<input
type="date"
value={customStartDate}
onChange={(e) => setCustomStartDate(e.target.value)}
className="px-3 py-1.5 text-sm border border-border rounded-md bg-card shadow-sm"
/>
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Date de fin</label>
<input
type="date"
value={customEndDate}
onChange={(e) => setCustomEndDate(e.target.value)}
max={new Date().toISOString().split('T')[0]}
className="px-3 py-1.5 text-sm border border-border rounded-md bg-card shadow-sm"
/>
</div>
</div>
)}
{/* FORCE RESCAN CHECKBOX */}
<label className="flex items-center gap-2 text-sm text-slate-600 cursor-pointer">
<input
type="checkbox"
checked={forceRescan}
onChange={(e) => setForceRescan(e.target.checked)}
className="rounded border-slate-300"
/>
Forcer le re-scan (re-traiter les courriels déjà importés)
</label>
{/* ═══ PROGRESS BAR (during scan) ═══ */}
{isScanning && progress && (
<div className="bg-card rounded-lg border border-border p-4 shadow-sm">
{/* Progress bar */}
<div className="w-full bg-slate-100 rounded-full h-3 mb-2">
<div
className="bg-blue-500 h-3 rounded-full transition-all duration-300 ease-out"
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className="flex justify-between text-sm text-slate-600">
<span>
{progress.processed}/{progress.total} courriels traités
</span>
<span>{progressPercent}%</span>
</div>
{progress.latest && (
<div className="mt-2 text-xs text-slate-500">
Dernier traité: {progress.latest.sender} — {progress.latest.amount?.toFixed(2)} $ → {progress.latest.branch}
</div>
)}
{progress.currentProvider && (
<div className="mt-1 text-xs text-slate-400">
Fournisseur IA: {progress.currentProvider}
</div>
)}
{progress.errored > 0 && (
<div className="mt-1 text-xs text-red-500">
{progress.errored} erreur(s) de traitement
</div>
)}
</div>
)}
{/* ═══ SCAN RESULT (after completion) ═══ */}
{result && !isScanning && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-sm">
<div className="font-medium text-green-800 mb-1">Scan terminé ✓</div>
<div className="text-green-700">
<span className="font-semibold">{result.parsed}</span> nouveaux virements importés
{result.skipped > 0 && (
<> · {result.skipped} déjà en base</>
)}
{result.errors > 0 && (
<> · <span className="text-red-600">{result.errors} erreurs</span></>
)}
</div>
</div>
)}
{/* ═══ ERROR MESSAGE ═══ */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">
<div className="font-medium mb-1">Erreur de scan</div>
{error}
</div>
)}
</div>
);
}