Álvaro Valenzuela Valdes commited on
Commit ·
cb1e16d
1
Parent(s): 2fc224f
feat: add Portfolio metric to Dashboard and toggle button to Detail View
Browse files
frontend/app/page.tsx
CHANGED
|
@@ -50,7 +50,26 @@ export default function HomePage() {
|
|
| 50 |
const [status, setStatus] = useState("listening");
|
| 51 |
const [searchKeyword, setSearchKeyword] = useState("");
|
| 52 |
const [lang, setLang] = useState<Language>("en");
|
|
|
|
| 53 |
const contentRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
const t = translations[lang];
|
| 56 |
|
|
@@ -254,7 +273,8 @@ export default function HomePage() {
|
|
| 254 |
tendersFound={tenders.length}
|
| 255 |
recommendedOpportunities={analysisResult?.decision === "Recommended" ? 1 : 0}
|
| 256 |
highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
|
| 257 |
-
reportsGenerated={
|
|
|
|
| 258 |
tenders={tenders}
|
| 259 |
onFilterClick={handleFilterClick}
|
| 260 |
lang={lang}
|
|
|
|
| 50 |
const [status, setStatus] = useState("listening");
|
| 51 |
const [searchKeyword, setSearchKeyword] = useState("");
|
| 52 |
const [lang, setLang] = useState<Language>("en");
|
| 53 |
+
const [followedCount, setFollowedCount] = useState(0);
|
| 54 |
const contentRef = useRef<HTMLDivElement>(null);
|
| 55 |
+
|
| 56 |
+
// Sync followed count from localStorage
|
| 57 |
+
useEffect(() => {
|
| 58 |
+
const updateFollowed = () => {
|
| 59 |
+
const saved = localStorage.getItem('andes_followed_tenders_full');
|
| 60 |
+
if (saved) {
|
| 61 |
+
setFollowedCount(JSON.parse(saved).length);
|
| 62 |
+
}
|
| 63 |
+
};
|
| 64 |
+
updateFollowed();
|
| 65 |
+
window.addEventListener('storage', updateFollowed);
|
| 66 |
+
// Also poll slightly for local changes if needed
|
| 67 |
+
const interval = setInterval(updateFollowed, 2000);
|
| 68 |
+
return () => {
|
| 69 |
+
window.removeEventListener('storage', updateFollowed);
|
| 70 |
+
clearInterval(interval);
|
| 71 |
+
};
|
| 72 |
+
}, []);
|
| 73 |
|
| 74 |
const t = translations[lang];
|
| 75 |
|
|
|
|
| 273 |
tendersFound={tenders.length}
|
| 274 |
recommendedOpportunities={analysisResult?.decision === "Recommended" ? 1 : 0}
|
| 275 |
highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
|
| 276 |
+
reportsGenerated={analysisHistory.length}
|
| 277 |
+
followedTendersCount={followedCount}
|
| 278 |
tenders={tenders}
|
| 279 |
onFilterClick={handleFilterClick}
|
| 280 |
lang={lang}
|
frontend/components/Dashboard.tsx
CHANGED
|
@@ -11,6 +11,7 @@ type Props = {
|
|
| 11 |
recommendedOpportunities: number;
|
| 12 |
highRiskItems: number;
|
| 13 |
reportsGenerated: number;
|
|
|
|
| 14 |
tenders: Tender[];
|
| 15 |
onFilterClick?: (type: "sector" | "region", value: string) => void;
|
| 16 |
lang: Language;
|
|
@@ -21,6 +22,7 @@ export default function Dashboard({
|
|
| 21 |
recommendedOpportunities,
|
| 22 |
highRiskItems,
|
| 23 |
reportsGenerated,
|
|
|
|
| 24 |
tenders,
|
| 25 |
onFilterClick,
|
| 26 |
lang
|
|
@@ -133,6 +135,7 @@ export default function Dashboard({
|
|
| 133 |
<StatCard title={t.tendersFound} value={tendersFound} subtitle={t.activeOpps} />
|
| 134 |
<StatCard title={t.recommended} value={recommendedOpportunities} subtitle="Fit score > 80%" />
|
| 135 |
<StatCard title={t.highRisk} value={highRiskItems} subtitle="Riesgos críticos" />
|
|
|
|
| 136 |
<StatCard title={t.totalPipeline} value={formatAmount(totalAmount)} subtitle="Monto total proyectado" />
|
| 137 |
</div>
|
| 138 |
|
|
|
|
| 11 |
recommendedOpportunities: number;
|
| 12 |
highRiskItems: number;
|
| 13 |
reportsGenerated: number;
|
| 14 |
+
followedTendersCount: number;
|
| 15 |
tenders: Tender[];
|
| 16 |
onFilterClick?: (type: "sector" | "region", value: string) => void;
|
| 17 |
lang: Language;
|
|
|
|
| 22 |
recommendedOpportunities,
|
| 23 |
highRiskItems,
|
| 24 |
reportsGenerated,
|
| 25 |
+
followedTendersCount,
|
| 26 |
tenders,
|
| 27 |
onFilterClick,
|
| 28 |
lang
|
|
|
|
| 135 |
<StatCard title={t.tendersFound} value={tendersFound} subtitle={t.activeOpps} />
|
| 136 |
<StatCard title={t.recommended} value={recommendedOpportunities} subtitle="Fit score > 80%" />
|
| 137 |
<StatCard title={t.highRisk} value={highRiskItems} subtitle="Riesgos críticos" />
|
| 138 |
+
<StatCard title="Portfolio" value={followedTendersCount} subtitle="Opps. guardadas" />
|
| 139 |
<StatCard title={t.totalPipeline} value={formatAmount(totalAmount)} subtitle="Monto total proyectado" />
|
| 140 |
</div>
|
| 141 |
|
frontend/components/TenderSearch.tsx
CHANGED
|
@@ -220,7 +220,15 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 220 |
<span className="text-2xl group-hover:-translate-x-1 transition-transform">←</span>
|
| 221 |
<span className="text-xs font-black uppercase tracking-widest">Back to search</span>
|
| 222 |
</button>
|
| 223 |
-
<div className="flex bg-white/5 p-1 rounded-2xl border border-white/10">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
<button onClick={() => setActiveDetailTab("Overview")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Overview" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}>Overview</button>
|
| 225 |
<button onClick={() => setActiveDetailTab("Agent Chat")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Agent Chat" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}>Agent Chat</button>
|
| 226 |
</div>
|
|
|
|
| 220 |
<span className="text-2xl group-hover:-translate-x-1 transition-transform">←</span>
|
| 221 |
<span className="text-xs font-black uppercase tracking-widest">Back to search</span>
|
| 222 |
</button>
|
| 223 |
+
<div className="flex items-center gap-3 bg-white/5 p-1 rounded-2xl border border-white/10">
|
| 224 |
+
<button
|
| 225 |
+
onClick={() => toggleFollow(tender)}
|
| 226 |
+
className={`px-4 py-2.5 rounded-xl text-[10px] font-black uppercase transition-all flex items-center gap-2 ${followedCodes.includes(tender.code) ? "bg-amber-500/20 text-amber-400 border border-amber-500/30" : "bg-white/5 text-slate-400 hover:bg-white/10"}`}
|
| 227 |
+
>
|
| 228 |
+
<span>{followedCodes.includes(tender.code) ? "★" : "☆"}</span>
|
| 229 |
+
<span>{followedCodes.includes(tender.code) ? "In Portfolio" : "Add to Portfolio"}</span>
|
| 230 |
+
</button>
|
| 231 |
+
<div className="w-px h-6 bg-white/10 mx-1" />
|
| 232 |
<button onClick={() => setActiveDetailTab("Overview")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Overview" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}>Overview</button>
|
| 233 |
<button onClick={() => setActiveDetailTab("Agent Chat")} className={`px-6 py-2.5 rounded-xl text-xs font-black uppercase transition-all ${activeDetailTab === "Agent Chat" ? "bg-purple-600 text-white shadow-lg" : "text-slate-500"}`}>Agent Chat</button>
|
| 234 |
</div>
|