import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AlertTriangle, Building2, CheckCircle2, Landmark, Loader2, MapPin, RefreshCw, Rocket, Scale, Sparkles, Target, } from 'lucide-react'; import toast from 'react-hot-toast'; import { getStrategyRecommend, postStrategyRecommend, type StrategyRecommendResult, } from '../../api/client'; import { formatScore, formatTaxChecklistId, hasTaxChecklists, isBlockedPath, normalizeStrategyResult, pathsForCards, resolveAutoSelectedPath, type StrategyPathView, type StrategyResultView, } from './strategyCascadeModel'; export type StrategyCascadePanelProps = { /** Company NIP — enables GET with profile spine cache */ nip?: string; /** Initial project goal (B+R, inwestycja, …) */ initialGoal?: string; /** Initial region / voivodeship */ initialRegion?: string; /** Optional company_data for POST when no profile spine yet */ companyData?: Record; /** Prefer POST (wizard) vs GET (profile page with saved spine) */ mode?: 'get' | 'post' | 'auto'; /** Compact layout for wizard / sidebar */ compact?: boolean; /** Auto-fetch when nip/goal/region settle */ autoLoad?: boolean; /** Notify parent of selected path id (user click) */ onPathSelect?: (pathId: string, path: StrategyPathView) => void; /** Currently selected path (controlled); defaults to primary */ selectedPathId?: string | null; }; const PATH_ICONS: Record = { tax_psi_ulgi: , regional_fst: , national_feng: , debt_loan_guarantee: , direct_eu_prep: , }; function pathIcon(id: string) { return PATH_ICONS[id] || ; } /** * Strategy cascade 2026 — ≥4 path cards from GET|POST /api/company/strategy/recommend. * Marks primary; shows de_minimis_blocked paths; goal + region inputs. */ const StrategyCascadePanel: React.FC = ({ nip = '', initialGoal = '', initialRegion = '', companyData, mode = 'auto', compact = false, autoLoad = true, onPathSelect, selectedPathId: controlledSelected, }) => { const [goal, setGoal] = useState(initialGoal); const [region, setRegion] = useState(initialRegion); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [view, setView] = useState(null); const [localSelected, setLocalSelected] = useState(null); /** True after the user clicks a path card (not auto-primary). */ const userPickedRef = useRef(false); const localSelectedRef = useRef(null); const onPathSelectRef = useRef(onPathSelect); onPathSelectRef.current = onPathSelect; const controlledRef = useRef(controlledSelected); controlledRef.current = controlledSelected; // Sync when parent changes initial goal/region (wizard step transitions) useEffect(() => { setGoal(initialGoal); }, [initialGoal]); useEffect(() => { setRegion(initialRegion); }, [initialRegion]); const applyResult = useCallback( (strategy: StrategyRecommendResult | Record) => { const next = normalizeStrategyResult(strategy as Record); setView(next); const controlled = controlledRef.current; const current = controlled !== undefined ? controlled : localSelectedRef.current; const auto = resolveAutoSelectedPath(next, current, userPickedRef.current); if (auto) { localSelectedRef.current = auto.id; setLocalSelected(auto.id); // Notify parent of primary (or fallback) when nothing usable was selected yet. // Fixes create-project omitting strategy_path when user never clicked a card. onPathSelectRef.current?.(auto.id, auto); } else if (!current && next.primaryPathId) { localSelectedRef.current = next.primaryPathId; setLocalSelected(next.primaryPathId); } }, [], ); const load = useCallback( async (overrides?: { goal?: string; region?: string }) => { const goalEff = overrides?.goal ?? goal; const regionEff = overrides?.region ?? region; setLoading(true); setError(null); try { const cleanedNip = (nip || '').replace(/\D/g, ''); const usePost = mode === 'post' || (mode === 'auto' && (!!companyData || (!cleanedNip && (!!goalEff || !!regionEff)))); let strategy: StrategyRecommendResult; if (usePost) { const res = await postStrategyRecommend({ nip: cleanedNip || undefined, goal: goalEff || '', region: regionEff || '', company_data: companyData, }); strategy = res.strategy; } else { const res = await getStrategyRecommend({ nip: cleanedNip || undefined, goal: goalEff || '', region: regionEff || '', }); strategy = res.strategy; } applyResult(strategy); } catch (e: unknown) { console.error('strategy/recommend failed', e); const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || 'Nie udało się pobrać kaskady strategii.'; setError(String(msg)); toast.error('Błąd kaskady strategii'); } finally { setLoading(false); } }, [nip, goal, region, companyData, mode, applyResult], ); // Auto-load on mount / when nip or parent goal/region seed changes (not every keystroke). useEffect(() => { if (!autoLoad) return; setGoal(initialGoal); setRegion(initialRegion); const t = window.setTimeout(() => { void load({ goal: initialGoal, region: initialRegion }); }, 250); return () => window.clearTimeout(t); // eslint-disable-next-line react-hooks/exhaustive-deps -- goal/region edits use Odśwież or Enter }, [autoLoad, nip, initialGoal, initialRegion, mode]); const selectedId = controlledSelected !== undefined ? controlledSelected : localSelected; const cards = view ? pathsForCards(view) : []; const handleSelect = (path: StrategyPathView) => { if (isBlockedPath(path)) { toast.error('Ścieżka zablokowana (de minimis) — wybierz inną rekomendację.'); return; } userPickedRef.current = true; localSelectedRef.current = path.id; setLocalSelected(path.id); onPathSelect?.(path.id, path); }; return (
Kaskada strategii 2026 {view?.deMinimisExhausted && ( de minimis wyczerpany )} {view?.mspStatus && ( MŚP: {view.mspStatus} )}
setGoal(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && void load()} placeholder="np. B+R, inwestycja, OZE, deeptech…" style={{ width: '100%', padding: '0.55rem 0.75rem 0.55rem 2rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', fontSize: '0.9rem', }} />
setRegion(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && void load()} placeholder="np. śląskie, małopolskie…" style={{ width: '100%', padding: '0.55rem 0.75rem 0.55rem 2rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', fontSize: '0.9rem', }} />
{error && (
{error}
)} {loading && !view && (

Liczenie kaskady ścieżek…

)} {cards.length > 0 && (
{cards.map((path) => { const blocked = isBlockedPath(path); const isPrimary = Boolean(path.primary); const isSelected = selectedId === path.id; return ( ); })}
)} {!loading && view && cards.length === 0 && (

Brak ścieżek strategii — spróbuj odświeżyć lub uzupełnij cel/region.

)} {view?.notes && view.notes.length > 0 && !compact && (
    {view.notes.map((n, i) => (
  • {n}
  • ))}
)}
); }; export default StrategyCascadePanel;