Spaces:
Running
Running
| import React, { useEffect, useState } from 'react'; | |
| import { MapPin, ExternalLink, CalendarPlus, Filter } from 'lucide-react'; | |
| import toast from 'react-hot-toast'; | |
| import { getRegionalPrograms, RegionalProgram, RegionalProgramsResponse, createPipelineEntry } from '../../api/client'; | |
| interface Props { | |
| projectId?: string; | |
| compact?: boolean; | |
| } | |
| const RegionalProgramsPanel: React.FC<Props> = ({ projectId, compact = false }) => { | |
| const [programs, setPrograms] = useState<RegionalProgram[]>([]); | |
| const [voivodeships, setVoivodeships] = useState<string[]>([]); | |
| const [bipLiveCount, setBipLiveCount] = useState(0); | |
| const [bipParserVoivodeships, setBipParserVoivodeships] = useState<string[]>([]); | |
| const [selectedVoivodeship, setSelectedVoivodeship] = useState(''); | |
| const [loading, setLoading] = useState(true); | |
| const load = async (voivodeship?: string) => { | |
| try { | |
| const data: RegionalProgramsResponse = await getRegionalPrograms(voivodeship || undefined); | |
| setPrograms(data.programs || []); | |
| setVoivodeships(data.voivodeships || []); | |
| setBipLiveCount(data.bip_live_count ?? 0); | |
| setBipParserVoivodeships(data.bip_parser_voivodeships || []); | |
| } catch { | |
| toast.error('Nie udało się załadować katalogu RPO'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| setLoading(true); | |
| load(selectedVoivodeship || undefined); | |
| }, [selectedVoivodeship]); | |
| const handleAddToPipeline = async (prog: RegionalProgram) => { | |
| if (!projectId) { | |
| toast.error('Wybierz projekt, aby dodać do pipeline'); | |
| return; | |
| } | |
| try { | |
| await createPipelineEntry(projectId, { | |
| grant_name: prog.name, | |
| grant_source_id: prog.id, | |
| operator: prog.operator, | |
| status: 'planned', | |
| deadline_alert: true, | |
| notes: `RPO ${prog.voivodeship} — katalog regionalny`, | |
| }); | |
| toast.success(`„${prog.program}” dodano do pipeline`); | |
| } catch { | |
| toast.error('Błąd dodawania do pipeline'); | |
| } | |
| }; | |
| if (loading) { | |
| return <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>Ładowanie programów RPO…</p>; | |
| } | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}> | |
| <MapPin size={18} color="#a78bfa" /> | |
| <span style={{ fontWeight: 700, fontSize: compact ? '0.95rem' : '1.05rem' }}> | |
| Katalog RPO wojewódzkich | |
| </span> | |
| <span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>({programs.length})</span> | |
| {bipLiveCount > 0 && ( | |
| <span | |
| style={{ | |
| fontSize: '0.7rem', | |
| background: 'rgba(34,197,94,0.12)', | |
| color: '#4ade80', | |
| padding: '2px 8px', | |
| borderRadius: 999, | |
| }} | |
| title="Programy z live cache BIP (credibility gate)" | |
| > | |
| BIP live: {bipLiveCount} | |
| </span> | |
| )} | |
| </div> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}> | |
| {bipParserVoivodeships.length > 0 && ( | |
| <span style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}> | |
| Parser BIP: {bipParserVoivodeships.join(', ')} | |
| </span> | |
| )} | |
| <Filter size={14} color="var(--text-muted)" /> | |
| <select | |
| className="form-input" | |
| value={selectedVoivodeship} | |
| onChange={(e) => setSelectedVoivodeship(e.target.value)} | |
| style={{ fontSize: '0.85rem', padding: '0.35rem 0.6rem', minWidth: 160 }} | |
| > | |
| <option value="">Wszystkie województwa</option> | |
| {voivodeships.map((v) => ( | |
| <option key={v} value={v}>{v.charAt(0).toUpperCase() + v.slice(1)}</option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem', maxHeight: compact ? 320 : undefined, overflowY: compact ? 'auto' : undefined }}> | |
| {programs.map((prog) => ( | |
| <div | |
| key={prog.id} | |
| style={{ | |
| background: 'rgba(255,255,255,0.025)', | |
| border: '1px solid rgba(255,255,255,0.06)', | |
| borderRadius: 10, | |
| padding: compact ? '0.65rem 0.75rem' : '0.85rem 1rem', | |
| }} | |
| > | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '0.75rem' }}> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ fontWeight: 700, fontSize: '0.9rem', marginBottom: '0.25rem' }}>{prog.name}</div> | |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.35rem' }}> | |
| {prog.operator} • {prog.voivodeship} | |
| </div> | |
| {prog.topics && ( | |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}> | |
| {prog.topics.slice(0, 3).map((t) => ( | |
| <span | |
| key={t} | |
| style={{ | |
| fontSize: '0.65rem', | |
| background: 'rgba(167,139,250,0.15)', | |
| color: '#a78bfa', | |
| padding: '2px 6px', | |
| borderRadius: 4, | |
| }} | |
| > | |
| {t} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div style={{ display: 'flex', gap: '0.35rem', flexShrink: 0 }}> | |
| {prog.url && ( | |
| <a | |
| href={prog.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="btn btn-secondary" | |
| style={{ padding: '0.35rem', fontSize: '0.75rem' }} | |
| title="Strona operatora" | |
| > | |
| <ExternalLink size={14} /> | |
| </a> | |
| )} | |
| {projectId && ( | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => handleAddToPipeline(prog)} | |
| style={{ padding: '0.35rem', fontSize: '0.75rem' }} | |
| title="Dodaj do pipeline" | |
| > | |
| <CalendarPlus size={14} /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default RegionalProgramsPanel; | |