import { useCallback, useEffect, useState } from 'react'; import { Activity, AlertTriangle, CheckCircle2, Clock, ExternalLink, Loader2, RefreshCw, Search, ShieldCheck, Sparkles, } from 'lucide-react'; import toast from 'react-hot-toast'; import { getFirmPulse, getGrantPulse, PulseItem, PulseKind, PulseResponse, } from '../api/client'; const KINDS: { id: PulseKind; label: string; hint: string; color: string }[] = [ { id: 'current', label: 'Aktualne', hint: 'Otwarte nabory', color: '#34d399' }, { id: 'new', label: 'Nowe', hint: 'Ostatnio w katalogu', color: '#60a5fa' }, { id: 'closing', label: 'Zamykające się', hint: 'Termin wkrótce', color: '#f87171' }, { id: 'planned', label: 'Planowane', hint: 'Zapowiedziane', color: '#a78bfa' }, { id: 'all', label: 'Wszystkie', hint: 'Pełny puls', color: '#94a3b8' }, ]; const FlagPill: React.FC<{ ok?: boolean; label: string }> = ({ ok, label }) => ( {ok ? : } {label} ); const PulseCard: React.FC<{ item: PulseItem; showMatch?: boolean }> = ({ item, showMatch }) => { const v = item.verification || {}; const daysLeft = (() => { if (!item.deadline) return null; const t = new Date(item.deadline).getTime(); if (Number.isNaN(t)) return null; return Math.ceil((t - Date.now()) / 86400000); })(); return (
{(item.pulse_kinds || []).map((k) => ( {k} ))} {item.alert_worthy && ( ALERT-READY )}

{item.name || 'Bez nazwy'}

{item.operator || item.source || '—'} {item.status ? ` · ${item.status}` : ''}
{showMatch && typeof item.match_score === 'number' && (
{item.match_score}%
)} {daysLeft !== null && (
{daysLeft > 0 ? `${daysLeft} dni` : item.continuous ? 'ciągły' : 'termin minął'}
)} {item.deadline ? (
do {item.deadline}
) : (
{item.continuous ? 'nabór ciągły' : 'brak terminu (uczciwie puste)'}
)}
{item.why_it_fits && (

{item.why_it_fits}

)} {!!item.generation_blockers?.length && ( )}
{item.url && ( Strona naboru )} {item.regulation_url && ( Regulamin )}
); }; export default function Pulse() { const [kind, setKind] = useState('current'); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [nip, setNip] = useState(''); const [description, setDescription] = useState(''); const [firmMode, setFirmMode] = useState(false); const [firmLoading, setFirmLoading] = useState(false); const [firmData, setFirmData] = useState<(PulseResponse & { gaps?: string[]; company?: Record; alert_count?: number }) | null>(null); const loadPulse = useCallback(async (k: PulseKind) => { setLoading(true); try { const res = await getGrantPulse(k, 50); setData(res); } catch (e: unknown) { const status = (e as { response?: { status?: number } })?.response?.status; if (status === 401) { toast.error('Zaloguj się, aby zobaczyć Puls naborów.'); } else { toast.error('Nie udało się pobrać Pulsu.'); } setData(null); } finally { setLoading(false); } }, []); useEffect(() => { if (!firmMode) { void loadPulse(kind); } }, [kind, firmMode, loadPulse]); const runFirm = async () => { const cleanNip = nip.replace(/\D/g, ''); if (cleanNip.length !== 10 && !description.trim()) { toast.error('Podaj NIP (10 cyfr) lub opis inwestycji.'); return; } setFirmLoading(true); setFirmMode(true); try { const res = await getFirmPulse({ nip: cleanNip || undefined, description: description.trim() || undefined, kind, limit: 20, min_score: 35, }); setFirmData(res); toast.success(`Dopasowano ${res.count ?? 0} naborów`); } catch { toast.error('Firm Pulse niedostępny — sprawdź logowanie i NIP.'); setFirmData(null); } finally { setFirmLoading(false); } }; const items = firmMode ? firmData?.items || [] : data?.items || []; const counts = firmMode ? firmData?.counts : data?.counts; const quality = firmMode ? firmData?.quality : data?.quality; return (

Puls naborów

Ciągły podgląd katalogu: nowe, planowane, zamykające się i aktualne nabory z flagami weryfikacji (termin · status · regulamin). Dane z API — bez mocków.

{KINDS.map((k) => ( ))}
Doradca firmowy (Firm Pulse)
setNip(e.target.value)} placeholder="NIP (10 cyfr)" inputMode="numeric" style={{ flex: '0 0 160px', padding: '0.65rem 0.8rem', borderRadius: 10, border: '1px solid var(--border-subtle)', background: 'var(--bg-base)', color: 'var(--text-primary)', }} /> setDescription(e.target.value)} placeholder="Opis inwestycji / branża (opcjonalnie)" style={{ flex: 1, minWidth: 200, padding: '0.65rem 0.8rem', borderRadius: 10, border: '1px solid var(--border-subtle)', background: 'var(--bg-base)', color: 'var(--text-primary)', }} />
{firmMode && firmData?.company && (
Firma: {(firmData.company as { name?: string }).name || '—'} · NIP{' '} {(firmData.company as { nip?: string }).nip || nip || '—'} {typeof firmData.alert_count === 'number' ? ` · alert-ready: ${firmData.alert_count}` : ''} {!!firmData.gaps?.length && (
Luki rejestrów: {firmData.gaps.slice(0, 3).join(' · ')}
)}
)}
{quality && (
terminy: {Math.round((quality.deadline_rate || 0) * 100)}% regulaminy: {Math.round((quality.regulation_rate || 0) * 100)}% {quality.deadline_backfilled ? ( backfill z tekstu: {quality.deadline_backfilled} ) : null}
)} {(loading || firmLoading) && (
Ładowanie pulsu…
)} {!loading && !firmLoading && items.length === 0 && (
Brak pozycji dla wybranego rodzaju. Spróbuj „Aktualne” lub odśwież katalog naborów.
)}
{items.map((it) => ( ))}
); }