Á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 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
- <div key={sector}>
142
- <div className="flex justify-between text-xs mb-1">
143
- <span className="text-slate-300">{sector}</span>
144
- <span className="text-cyan font-semibold">{count}</span>
 
 
 
 
145
  </div>
146
- <div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
147
  <div
148
- className="h-full bg-cyan transition-all duration-500"
149
  style={{ width: `${(count / tenders.length) * 100}%` }}
150
  />
151
  </div>
152
- </div>
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
- <div key={region}>
167
- <div className="flex justify-between text-xs mb-1">
168
- <span className="text-slate-300">{region}</span>
169
- <span className="text-sky font-semibold">{count}</span>
 
 
 
 
170
  </div>
171
- <div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
172
  <div
173
- className="h-full bg-sky transition-all duration-500"
174
  style={{ width: `${(count / tenders.length) * 100}%` }}
175
  />
176
  </div>
177
- </div>
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-6 py-5 w-[140px]">Selection</th>
201
- <th className="px-6 py-5 w-[300px]">Opportunity</th>
202
- <th className="px-6 py-5 w-[200px]">Buyer</th>
203
- <th className="px-6 py-5 text-center w-[120px]">Status</th>
204
- <th className="px-6 py-5 text-right w-[120px]">Deadline</th>
205
- <th className="px-6 py-5 text-right pr-10 w-[120px]">Actions</th>
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-6 py-5">
216
- <div className="flex items-center gap-3">
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-4 h-4 rounded border-white/10 bg-white/5 text-purple-500 focus:ring-purple-500/40"
225
  />
226
  <button
227
  onClick={(e) => {
228
  e.stopPropagation();
229
  toggleFollow(tender.code);
230
  }}
231
- className={`text-lg 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'}`}
232
  >
233
  {followedCodes.includes(tender.code) ? "★" : "☆"}
234
  </button>
235
- <span className="font-mono text-purple-400 text-[10px]">{tender.code}</span>
236
  </div>
237
  </td>
238
- <td className="px-6 py-5 max-w-xs">
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-[10px] text-slate-500">{tender.region || "Nacional"}</span>
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-6 py-5 text-slate-400 text-xs">{tender.buyer}</td>
246
- <td className="px-6 py-5 text-center">
247
- <span className={`inline-block rounded-full px-3 py-1 text-[10px] font-bold ${
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-6 py-5 text-right font-mono text-xs text-slate-400">
254
  {tender.closing_date ? new Date(tender.closing_date).toLocaleDateString() : "---"}
255
  </td>
256
- <td className="px-6 py-5 text-right pr-10">
257
  <button
258
  onClick={(e) => {
259
  e.stopPropagation();
260
  onAnalyze(tender);
261
  }}
262
- className="bg-white/10 hover:bg-white/20 text-white text-[11px] font-bold px-4 py-2 rounded-lg transition-all border border-white/10"
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>