import { useState, useEffect, useMemo } from 'react'; import { Search, RefreshCw, ExternalLink, Filter, TrendingUp, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react'; import { getGrantNabory } from '../api/client'; import toast from 'react-hot-toast'; interface Nabor { id: string; name: string; program: string; subprogram?: string; type?: string; status: string; deadline?: string; max_dofinansowanie_pln?: number; min_dofinansowanie_pln?: number; dofinansowanie_pct_max?: number; eligible_regions?: string[]; eligible_company_sizes?: string[]; description?: string; url?: string; source?: string; legal_source?: string; fetched_at?: string; last_verified?: string; is_outdated_warning?: boolean; eurlex_url?: string; official_doc_url?: string; } const BadgeProgram: React.FC<{ program: string }> = ({ program }) => { const colors: Record = { PARP: '#3b82f6', NCBR: '#8b5cf6', FENG: '#10b981', KPO: '#f59e0b', POPW: '#ec4899', }; const color = colors[program] || '#6b7280'; return ( {program} ); }; const formatPLN = (v?: number) => v !== undefined ? `${(v / 1_000_000).toFixed(1)} mln zł` : '—'; const NaborCard: React.FC<{ nabor: Nabor }> = ({ nabor }) => { const [expanded, setExpanded] = useState(false); const daysLeft = useMemo(() => { if (!nabor.deadline) return null; const time = new Date(nabor.deadline).getTime(); if (isNaN(time)) return null; return Math.ceil((time - Date.now()) / 86400000); }, [nabor.deadline]); return (
{ (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-strong)'; (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 20px rgba(0,0,0,0.15)'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-subtle)'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }} > {/* Header */}
{nabor.type && ( {nabor.type} )}

{nabor.name}

{daysLeft !== null && (
{daysLeft > 0 ? `${daysLeft} dni` : 'Upłynął'}
)} {nabor.deadline && (
do {nabor.deadline}
)}
{/* Stats row */}
{nabor.dofinansowanie_pct_max && (
Dofinansowanie
do {nabor.dofinansowanie_pct_max}%
)} {nabor.max_dofinansowanie_pln && (
Maks. wartość
{formatPLN(nabor.max_dofinansowanie_pln)}
)} {nabor.min_dofinansowanie_pln && (
Min. wartość
{formatPLN(nabor.min_dofinansowanie_pln)}
)}
{/* Tags */} {nabor.eligible_company_sizes && nabor.eligible_company_sizes.length > 0 && (
{nabor.eligible_company_sizes.map(s => ( {s} ))} {nabor.eligible_regions?.slice(0, 2).map(r => ( 📍 {r} ))}
)} {/* Expandable description */} {nabor.description && ( <> {expanded && (

{nabor.description}

)} )} {/* Legal Basis (EUR-Lex) */} {nabor.legal_source && (
Weryfikacja Prawna (EUR-Lex)
{nabor.legal_source}
)} {/* Outdated Warning */} {nabor.is_outdated_warning && (
Ostrzeżenie o aktualności
Ten nabór mógł zostać niedawno zakończony lub wstrzymany (wykryto w automatycznej analizie treści).
)} {/* Actions */}
{nabor.url && ( Strona programu )} {nabor.eurlex_url && ( EUR-Lex )} {nabor.official_doc_url && ( Baza Funduszy )}
{nabor.last_verified && (
Dane aktualne na: {nabor.last_verified}
)}
); }; const PROGRAMS = ['Wszystkie', 'PARP', 'NCBR', 'FENG', 'KPO', 'POPW']; const SIZES = ['Wszystkie', 'mikro', 'małe', 'średnie', 'duże', 'MŚP']; const Nabory: React.FC = () => { const [nabory, setNabory] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [search, setSearch] = useState(''); const [filterProgram, setFilterProgram] = useState('Wszystkie'); const [filterSize, setFilterSize] = useState('Wszystkie'); const [lastFetch, setLastFetch] = useState(null); const loadNabory = async (forceRefresh = false) => { try { forceRefresh ? setRefreshing(true) : setLoading(true); const result = await getGrantNabory(forceRefresh); setNabory(result.nabory || []); setLastFetch(new Date().toLocaleTimeString('pl-PL')); setError(null); if (forceRefresh) toast.success('Cache odświeżony!'); } catch (e: any) { setError('Nie udało się pobrać listy naborów. Sprawdź połączenie z serwerem.'); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { loadNabory(); }, []); const filtered = useMemo(() => { return nabory.filter(n => { const q = search.toLowerCase(); if (q && !n.name.toLowerCase().includes(q) && !n.description?.toLowerCase().includes(q)) return false; if (filterProgram !== 'Wszystkie' && n.program !== filterProgram && !n.name.includes(filterProgram)) return false; if (filterSize !== 'Wszystkie') { const sizes = n.eligible_company_sizes?.map(s => s.toLowerCase()) || []; if (sizes.length > 0 && !sizes.includes(filterSize.toLowerCase()) && !sizes.includes('mśp')) return false; } return true; }); }, [nabory, search, filterProgram, filterSize]); return (
{/* Header */}

🏦 Aktywne Nabory Dotacji

Źródła: PARP + NCBR • Dane odświeżane co 24h {lastFetch && <> • Ostatnia aktualizacja: {lastFetch}}

{/* Filtry */}
setSearch(e.target.value)} style={{ width: '100%', paddingLeft: '2rem', background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)', borderRadius: '8px', padding: '0.5rem 0.75rem 0.5rem 2rem', color: 'var(--text-primary)', fontSize: '0.875rem', }} />
{/* Stats śummary */} {!loading && nabory.length > 0 && (
{[ { label: 'Wszystkich naborów', value: nabory.length, color: 'var(--accent-blue)' }, { label: 'Po filtrach', value: filtered.length, color: 'var(--accent-green)' }, { label: 'Programów', value: [...new Set(nabory.map(n => n.program))].length, color: 'var(--accent-purple)' }, ].map(stat => (
{stat.value}
{stat.label}
))}
)} {/* Content */} {loading ? (
{[1, 2, 3].map((i: number) => (
))}
) : error ? (
Błąd połączenia

{error}

) : filtered.length === 0 ? (

Brak naborów pasujących do kryteriów wyszukiwania.

) : (
{filtered.map(n => )}
)}
); }; export default Nabory;