Álvaro Valenzuela Valdes commited on
Commit ·
35c7f2f
1
Parent(s): aaf6916
feat: Interactive dashboard navigation and table layout optimization
Browse files- frontend/app/page.tsx +10 -0
- frontend/components/Dashboard.tsx +25 -15
- frontend/components/TenderSearch.tsx +27 -22
frontend/app/page.tsx
CHANGED
|
@@ -46,6 +46,7 @@ export default function HomePage() {
|
|
| 46 |
const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
|
| 47 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
| 48 |
const [status, setStatus] = useState("listening");
|
|
|
|
| 49 |
|
| 50 |
// Scroll to top when tab changes
|
| 51 |
useEffect(() => {
|
|
@@ -101,6 +102,13 @@ export default function HomePage() {
|
|
| 101 |
window.history.pushState({}, '', `?tab=agent_analysis`);
|
| 102 |
};
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
const handleSearch = async (params: { keyword?: string; buyer?: string; provider_code?: string; date?: string; skip?: number; limit?: number }) => {
|
| 105 |
const results = await searchTenders(params);
|
| 106 |
setTenders(results);
|
|
@@ -202,6 +210,7 @@ export default function HomePage() {
|
|
| 202 |
highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
|
| 203 |
reportsGenerated={analysisResult ? 1 : 0}
|
| 204 |
tenders={tenders}
|
|
|
|
| 205 |
/>
|
| 206 |
)}
|
| 207 |
{(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
|
|
@@ -210,6 +219,7 @@ export default function HomePage() {
|
|
| 210 |
onSearch={handleSearch}
|
| 211 |
onAnalyze={handleTenderSelect}
|
| 212 |
forceShowFollowed={activeTab === "My Portfolio"}
|
|
|
|
| 213 |
/>
|
| 214 |
)}
|
| 215 |
{activeTab === "Company Profile" && (
|
|
|
|
| 46 |
const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
|
| 47 |
const [analysisHistory, setAnalysisHistory] = useState<AnalysisHistoryItem[]>([]);
|
| 48 |
const [status, setStatus] = useState("listening");
|
| 49 |
+
const [searchKeyword, setSearchKeyword] = useState("");
|
| 50 |
|
| 51 |
// Scroll to top when tab changes
|
| 52 |
useEffect(() => {
|
|
|
|
| 102 |
window.history.pushState({}, '', `?tab=agent_analysis`);
|
| 103 |
};
|
| 104 |
|
| 105 |
+
const handleFilterClick = (type: "sector" | "region", value: string) => {
|
| 106 |
+
setSearchKeyword(value);
|
| 107 |
+
setActiveTab("Tender Search");
|
| 108 |
+
handleSearch({ keyword: value });
|
| 109 |
+
window.history.pushState({}, '', `?tab=tender_search&q=${encodeURIComponent(value)}`);
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
const handleSearch = async (params: { keyword?: string; buyer?: string; provider_code?: string; date?: string; skip?: number; limit?: number }) => {
|
| 113 |
const results = await searchTenders(params);
|
| 114 |
setTenders(results);
|
|
|
|
| 210 |
highRiskItems={analysisResult?.risks.filter(r => r.severity === "High").length ?? 0}
|
| 211 |
reportsGenerated={analysisResult ? 1 : 0}
|
| 212 |
tenders={tenders}
|
| 213 |
+
onFilterClick={handleFilterClick}
|
| 214 |
/>
|
| 215 |
)}
|
| 216 |
{(activeTab === "Tender Search" || activeTab === "My Portfolio") && (
|
|
|
|
| 219 |
onSearch={handleSearch}
|
| 220 |
onAnalyze={handleTenderSelect}
|
| 221 |
forceShowFollowed={activeTab === "My Portfolio"}
|
| 222 |
+
initialKeyword={searchKeyword}
|
| 223 |
/>
|
| 224 |
)}
|
| 225 |
{activeTab === "Company Profile" && (
|
frontend/components/Dashboard.tsx
CHANGED
|
@@ -10,6 +10,7 @@ type Props = {
|
|
| 10 |
highRiskItems: number;
|
| 11 |
reportsGenerated: number;
|
| 12 |
tenders: Tender[];
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
export default function Dashboard({
|
|
@@ -17,7 +18,8 @@ export default function Dashboard({
|
|
| 17 |
recommendedOpportunities,
|
| 18 |
highRiskItems,
|
| 19 |
reportsGenerated,
|
| 20 |
-
tenders
|
|
|
|
| 21 |
}: Props) {
|
| 22 |
const [isSyncing, setIsSyncing] = useState(false);
|
| 23 |
const [dbStatus, setDbStatus] = useState<any>(null);
|
|
@@ -138,18 +140,22 @@ export default function Dashboard({
|
|
| 138 |
<div className="space-y-4">
|
| 139 |
{sectorDistribution.length > 0 ? (
|
| 140 |
sectorDistribution.map(([sector, count]) => (
|
| 141 |
-
<
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
</div>
|
| 146 |
-
<div className="h-
|
| 147 |
<div
|
| 148 |
-
className="h-full bg-cyan transition-all duration-
|
| 149 |
style={{ width: `${(count / tenders.length) * 100}%` }}
|
| 150 |
/>
|
| 151 |
</div>
|
| 152 |
-
</
|
| 153 |
))
|
| 154 |
) : (
|
| 155 |
<p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
|
|
@@ -163,18 +169,22 @@ export default function Dashboard({
|
|
| 163 |
<div className="space-y-4">
|
| 164 |
{regionDistribution.length > 0 ? (
|
| 165 |
regionDistribution.map(([region, count]) => (
|
| 166 |
-
<
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
</div>
|
| 171 |
-
<div className="h-
|
| 172 |
<div
|
| 173 |
-
className="h-full bg-sky transition-all duration-
|
| 174 |
style={{ width: `${(count / tenders.length) * 100}%` }}
|
| 175 |
/>
|
| 176 |
</div>
|
| 177 |
-
</
|
| 178 |
))
|
| 179 |
) : (
|
| 180 |
<p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
|
|
|
|
| 10 |
highRiskItems: number;
|
| 11 |
reportsGenerated: number;
|
| 12 |
tenders: Tender[];
|
| 13 |
+
onFilterClick?: (type: "sector" | "region", value: string) => void;
|
| 14 |
};
|
| 15 |
|
| 16 |
export default function Dashboard({
|
|
|
|
| 18 |
recommendedOpportunities,
|
| 19 |
highRiskItems,
|
| 20 |
reportsGenerated,
|
| 21 |
+
tenders,
|
| 22 |
+
onFilterClick
|
| 23 |
}: Props) {
|
| 24 |
const [isSyncing, setIsSyncing] = useState(false);
|
| 25 |
const [dbStatus, setDbStatus] = useState<any>(null);
|
|
|
|
| 140 |
<div className="space-y-4">
|
| 141 |
{sectorDistribution.length > 0 ? (
|
| 142 |
sectorDistribution.map(([sector, count]) => (
|
| 143 |
+
<button
|
| 144 |
+
key={sector}
|
| 145 |
+
onClick={() => onFilterClick?.("sector", sector)}
|
| 146 |
+
className="w-full text-left group/item focus:outline-none"
|
| 147 |
+
>
|
| 148 |
+
<div className="flex justify-between text-xs mb-1.5">
|
| 149 |
+
<span className="text-slate-300 group-hover/item:text-cyan transition-colors">{sector}</span>
|
| 150 |
+
<span className="text-cyan font-semibold opacity-60 group-hover/item:opacity-100">{count}</span>
|
| 151 |
</div>
|
| 152 |
+
<div className="h-2 w-full bg-slate-900 rounded-full overflow-hidden border border-white/5">
|
| 153 |
<div
|
| 154 |
+
className="h-full bg-cyan transition-all duration-700 group-hover/item:brightness-125"
|
| 155 |
style={{ width: `${(count / tenders.length) * 100}%` }}
|
| 156 |
/>
|
| 157 |
</div>
|
| 158 |
+
</button>
|
| 159 |
))
|
| 160 |
) : (
|
| 161 |
<p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
|
|
|
|
| 169 |
<div className="space-y-4">
|
| 170 |
{regionDistribution.length > 0 ? (
|
| 171 |
regionDistribution.map(([region, count]) => (
|
| 172 |
+
<button
|
| 173 |
+
key={region}
|
| 174 |
+
onClick={() => onFilterClick?.("region", region)}
|
| 175 |
+
className="w-full text-left group/item focus:outline-none"
|
| 176 |
+
>
|
| 177 |
+
<div className="flex justify-between text-xs mb-1.5">
|
| 178 |
+
<span className="text-slate-300 group-hover/item:text-sky transition-colors">{region}</span>
|
| 179 |
+
<span className="text-sky font-semibold opacity-60 group-hover/item:opacity-100">{count}</span>
|
| 180 |
</div>
|
| 181 |
+
<div className="h-2 w-full bg-slate-900 rounded-full overflow-hidden border border-white/5">
|
| 182 |
<div
|
| 183 |
+
className="h-full bg-sky transition-all duration-700 group-hover/item:brightness-125"
|
| 184 |
style={{ width: `${(count / tenders.length) * 100}%` }}
|
| 185 |
/>
|
| 186 |
</div>
|
| 187 |
+
</button>
|
| 188 |
))
|
| 189 |
) : (
|
| 190 |
<p className="text-slate-500 text-xs italic">Sin datos disponibles.</p>
|
frontend/components/TenderSearch.tsx
CHANGED
|
@@ -9,10 +9,11 @@ type Props = {
|
|
| 9 |
onSearch: (params: { keyword?: string; buyer?: string; provider_code?: string; date?: string; skip?: number; limit?: number }) => void;
|
| 10 |
onAnalyze: (tender: Tender) => void;
|
| 11 |
forceShowFollowed?: boolean;
|
|
|
|
| 12 |
};
|
| 13 |
|
| 14 |
-
export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false }: Props) {
|
| 15 |
-
const [keyword, setKeyword] = useState(
|
| 16 |
const [buyerCode, setBuyerCode] = useState("");
|
| 17 |
const [date, setDate] = useState("");
|
| 18 |
const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
|
|
@@ -35,6 +36,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 35 |
if (forceShowFollowed) setShowOnlyFollowed(true);
|
| 36 |
}, [forceShowFollowed]);
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
useEffect(() => {
|
| 39 |
localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
|
| 40 |
}, [followedCodes]);
|
|
@@ -197,12 +202,12 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 197 |
<table className="w-full text-left text-sm table-fixed border-collapse">
|
| 198 |
<thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
|
| 199 |
<tr>
|
| 200 |
-
<th className="px-
|
| 201 |
-
<th className="px-
|
| 202 |
-
<th className="px-
|
| 203 |
-
<th className="px-
|
| 204 |
-
<th className="px-
|
| 205 |
-
<th className="px-
|
| 206 |
</tr>
|
| 207 |
</thead>
|
| 208 |
<tbody className="divide-y divide-white/5">
|
|
@@ -212,8 +217,8 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 212 |
className={`hover:bg-white/[0.04] cursor-pointer transition-colors group ${selectedCodes.includes(tender.code) ? 'bg-purple-500/5' : ''}`}
|
| 213 |
onClick={() => toggleExpanded(tender.code)}
|
| 214 |
>
|
| 215 |
-
<td className="px-
|
| 216 |
-
<div className="flex items-center gap-
|
| 217 |
<input
|
| 218 |
type="checkbox"
|
| 219 |
checked={selectedCodes.includes(tender.code)}
|
|
@@ -221,45 +226,45 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 221 |
e.stopPropagation();
|
| 222 |
toggleSelect(tender.code);
|
| 223 |
}}
|
| 224 |
-
className="w-
|
| 225 |
/>
|
| 226 |
<button
|
| 227 |
onClick={(e) => {
|
| 228 |
e.stopPropagation();
|
| 229 |
toggleFollow(tender.code);
|
| 230 |
}}
|
| 231 |
-
className={`text-
|
| 232 |
>
|
| 233 |
{followedCodes.includes(tender.code) ? "★" : "☆"}
|
| 234 |
</button>
|
| 235 |
-
<span className="font-mono text-purple-400 text-[
|
| 236 |
</div>
|
| 237 |
</td>
|
| 238 |
-
<td className="px-
|
| 239 |
-
<div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate">{tender.name}</div>
|
| 240 |
<div className="flex items-center gap-2 mt-1">
|
| 241 |
-
<span className="text-[
|
| 242 |
<span className="text-[8px] px-1.5 py-0.5 rounded-md bg-white/5 text-slate-600 border border-white/5 uppercase tracking-tighter">{tender.sector}</span>
|
| 243 |
</div>
|
| 244 |
</td>
|
| 245 |
-
<td className="px-
|
| 246 |
-
<td className="px-
|
| 247 |
-
<span className={`inline-block rounded-full px-
|
| 248 |
tender.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800/50 text-slate-500'
|
| 249 |
}`}>
|
| 250 |
{tender.status}
|
| 251 |
</span>
|
| 252 |
</td>
|
| 253 |
-
<td className="px-
|
| 254 |
{tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
|
| 255 |
</td>
|
| 256 |
-
<td className="px-
|
| 257 |
<button
|
| 258 |
onClick={(e) => {
|
| 259 |
e.stopPropagation();
|
| 260 |
onAnalyze(tender);
|
| 261 |
}}
|
| 262 |
-
className="bg-white/10 hover:bg-white/20 text-white text-[
|
| 263 |
>
|
| 264 |
Analyze
|
| 265 |
</button>
|
|
|
|
| 9 |
onSearch: (params: { keyword?: string; buyer?: string; provider_code?: string; date?: string; skip?: number; limit?: number }) => void;
|
| 10 |
onAnalyze: (tender: Tender) => void;
|
| 11 |
forceShowFollowed?: boolean;
|
| 12 |
+
initialKeyword?: string;
|
| 13 |
};
|
| 14 |
|
| 15 |
+
export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFollowed = false, initialKeyword = "" }: Props) {
|
| 16 |
+
const [keyword, setKeyword] = useState(initialKeyword);
|
| 17 |
const [buyerCode, setBuyerCode] = useState("");
|
| 18 |
const [date, setDate] = useState("");
|
| 19 |
const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
|
|
|
|
| 36 |
if (forceShowFollowed) setShowOnlyFollowed(true);
|
| 37 |
}, [forceShowFollowed]);
|
| 38 |
|
| 39 |
+
useEffect(() => {
|
| 40 |
+
if (initialKeyword) setKeyword(initialKeyword);
|
| 41 |
+
}, [initialKeyword]);
|
| 42 |
+
|
| 43 |
useEffect(() => {
|
| 44 |
localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
|
| 45 |
}, [followedCodes]);
|
|
|
|
| 202 |
<table className="w-full text-left text-sm table-fixed border-collapse">
|
| 203 |
<thead className="bg-white/5 text-slate-500 uppercase text-[10px] tracking-widest font-bold border-b border-white/5">
|
| 204 |
<tr>
|
| 205 |
+
<th className="px-4 py-5 w-[100px]">ID / Select</th>
|
| 206 |
+
<th className="px-4 py-5 w-[250px]">Opportunity</th>
|
| 207 |
+
<th className="px-4 py-5 w-[180px]">Buyer</th>
|
| 208 |
+
<th className="px-4 py-5 text-center w-[100px]">Status</th>
|
| 209 |
+
<th className="px-4 py-5 text-right w-[90px]">Deadline</th>
|
| 210 |
+
<th className="px-4 py-5 text-right pr-6 w-[100px]">Actions</th>
|
| 211 |
</tr>
|
| 212 |
</thead>
|
| 213 |
<tbody className="divide-y divide-white/5">
|
|
|
|
| 217 |
className={`hover:bg-white/[0.04] cursor-pointer transition-colors group ${selectedCodes.includes(tender.code) ? 'bg-purple-500/5' : ''}`}
|
| 218 |
onClick={() => toggleExpanded(tender.code)}
|
| 219 |
>
|
| 220 |
+
<td className="px-4 py-5">
|
| 221 |
+
<div className="flex items-center gap-2">
|
| 222 |
<input
|
| 223 |
type="checkbox"
|
| 224 |
checked={selectedCodes.includes(tender.code)}
|
|
|
|
| 226 |
e.stopPropagation();
|
| 227 |
toggleSelect(tender.code);
|
| 228 |
}}
|
| 229 |
+
className="w-3.5 h-3.5 rounded border-white/10 bg-white/5 text-purple-500 focus:ring-purple-500/40"
|
| 230 |
/>
|
| 231 |
<button
|
| 232 |
onClick={(e) => {
|
| 233 |
e.stopPropagation();
|
| 234 |
toggleFollow(tender.code);
|
| 235 |
}}
|
| 236 |
+
className={`text-base transition-all hover:scale-125 ${followedCodes.includes(tender.code) ? 'text-purple-400 drop-shadow-[0_0_8px_rgba(168,85,247,0.4)]' : 'text-slate-600 hover:text-slate-400'}`}
|
| 237 |
>
|
| 238 |
{followedCodes.includes(tender.code) ? "★" : "☆"}
|
| 239 |
</button>
|
| 240 |
+
<span className="font-mono text-purple-400 text-[9px] truncate">{tender.code}</span>
|
| 241 |
</div>
|
| 242 |
</td>
|
| 243 |
+
<td className="px-4 py-5">
|
| 244 |
+
<div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate text-xs">{tender.name}</div>
|
| 245 |
<div className="flex items-center gap-2 mt-1">
|
| 246 |
+
<span className="text-[9px] text-slate-500 truncate">{tender.region || "Nacional"}</span>
|
| 247 |
<span className="text-[8px] px-1.5 py-0.5 rounded-md bg-white/5 text-slate-600 border border-white/5 uppercase tracking-tighter">{tender.sector}</span>
|
| 248 |
</div>
|
| 249 |
</td>
|
| 250 |
+
<td className="px-4 py-5 text-slate-400 text-[11px] truncate">{tender.buyer}</td>
|
| 251 |
+
<td className="px-4 py-5 text-center">
|
| 252 |
+
<span className={`inline-block rounded-full px-2 py-0.5 text-[9px] font-bold ${
|
| 253 |
tender.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800/50 text-slate-500'
|
| 254 |
}`}>
|
| 255 |
{tender.status}
|
| 256 |
</span>
|
| 257 |
</td>
|
| 258 |
+
<td className="px-4 py-5 text-right font-mono text-[11px] text-slate-400">
|
| 259 |
{tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
|
| 260 |
</td>
|
| 261 |
+
<td className="px-4 py-5 text-right pr-6">
|
| 262 |
<button
|
| 263 |
onClick={(e) => {
|
| 264 |
e.stopPropagation();
|
| 265 |
onAnalyze(tender);
|
| 266 |
}}
|
| 267 |
+
className="bg-white/10 hover:bg-white/20 text-white text-[10px] font-bold px-3 py-1.5 rounded-lg transition-all border border-white/10"
|
| 268 |
>
|
| 269 |
Analyze
|
| 270 |
</button>
|