Álvaro Valenzuela Valdes commited on
Commit
a81ee34
·
1 Parent(s): 35c7f2f

feat: Replace expandable rows with detailed tender modal and simplify table

Browse files
Files changed (1) hide show
  1. frontend/components/TenderSearch.tsx +157 -122
frontend/components/TenderSearch.tsx CHANGED
@@ -16,7 +16,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
16
  const [keyword, setKeyword] = useState(initialKeyword);
17
  const [buyerCode, setBuyerCode] = useState("");
18
  const [date, setDate] = useState("");
19
- const [expandedTenderCodes, setExpandedTenderCodes] = useState<string[]>([]);
20
  const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
21
  const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
22
 
@@ -44,12 +44,6 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
44
  localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
45
  }, [followedCodes]);
46
 
47
- const toggleExpanded = (code: string) => {
48
- setExpandedTenderCodes((current) =>
49
- current.includes(code) ? current.filter((value) => value !== code) : [...current, code]
50
- );
51
- };
52
-
53
  const toggleFollow = (code: string) => {
54
  setFollowedCodes(prev =>
55
  prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]
@@ -64,7 +58,6 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
64
 
65
  const handleSyncToAgents = () => {
66
  setIsSyncingToAgents(true);
67
- // Simulate system ingestion
68
  setTimeout(() => {
69
  setIsSyncingToAgents(false);
70
  const firstSelected = tenders.find(t => t.code === selectedCodes[0]);
@@ -206,129 +199,54 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
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">
214
  {filteredTenders.map((tender) => (
215
- <Fragment key={tender.code}>
216
- <tr
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)}
225
- onChange={(e) => {
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>
271
- </td>
272
- </tr>
273
- {expandedTenderCodes.includes(tender.code) && (
274
- <tr className="bg-white/[0.01] animate-in fade-in duration-500 border-l-2 border-purple-500">
275
- <td colSpan={6} className="px-8 md:px-12 py-10">
276
- <div className="grid gap-10 lg:grid-cols-5 items-start">
277
- <div className="lg:col-span-3 space-y-8">
278
- <div>
279
- <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
280
- <span className="w-4 h-[1px] bg-purple-500/50" />
281
- Project Description
282
- </h4>
283
- <p className="text-slate-300 leading-relaxed text-sm bg-white/[0.02] p-5 rounded-2xl border border-white/5">{tender.description}</p>
284
- </div>
285
- <div className="grid grid-cols-2 gap-6">
286
- <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
287
- <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Estimated Amount</div>
288
- <div className="text-sm text-white font-bold">
289
- {tender.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP" }).format(tender.estimated_amount) : "Not Disclosed"}
290
- </div>
291
- </div>
292
- <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5 shadow-inner">
293
- <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Market Sector</div>
294
- <div className="text-sm text-white font-bold">{tender.sector || "General"}</div>
295
- </div>
296
- </div>
297
- </div>
298
- <div className="lg:col-span-2 space-y-8">
299
- <div>
300
- <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4 flex items-center gap-2">
301
- <span className="w-4 h-[1px] bg-purple-500/50" />
302
- Resources & Direct Links
303
- </h4>
304
- <div className="grid gap-3">
305
- {tender.attachments?.map((att, i) => (
306
- <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-4 rounded-2xl bg-white/[0.03] hover:bg-white/[0.08] border border-white/5 transition-all group/file">
307
- <div className="flex items-center gap-4">
308
- <span className="text-2xl">{att.name.endsWith('.pdf') ? "📕" : "📘"}</span>
309
- <div className="flex flex-col">
310
- <span className="text-xs font-bold text-slate-200 group-hover/file:text-white transition-colors">{att.name}</span>
311
- <span className="text-[10px] text-slate-500 uppercase tracking-tighter">Official Document</span>
312
- </div>
313
- </div>
314
- <span className="text-[10px] font-black text-purple-400 opacity-0 group-hover/file:opacity-100 transition-all">DOWNLOAD</span>
315
- </a>
316
- ))}
317
- <a
318
- href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${tender.code}`}
319
- target="_blank"
320
- className="flex items-center justify-center gap-2 p-3 rounded-xl bg-purple-500/10 border border-purple-500/20 text-purple-300 text-xs font-bold hover:bg-purple-500/20 transition mt-2"
321
- >
322
- Open official portal
323
- </a>
324
- </div>
325
- </div>
326
- </div>
327
- </div>
328
- </td>
329
- </tr>
330
- )}
331
- </Fragment>
332
  ))}
333
  </tbody>
334
  </table>
@@ -337,6 +255,123 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
337
  )}
338
  </div>
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  {isLoading && <BrandLoader />}
341
  </div>
342
  );
 
16
  const [keyword, setKeyword] = useState(initialKeyword);
17
  const [buyerCode, setBuyerCode] = useState("");
18
  const [date, setDate] = useState("");
19
+ const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
20
  const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
21
  const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
22
 
 
44
  localStorage.setItem('andes_followed_codes', JSON.stringify(followedCodes));
45
  }, [followedCodes]);
46
 
 
 
 
 
 
 
47
  const toggleFollow = (code: string) => {
48
  setFollowedCodes(prev =>
49
  prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code]
 
58
 
59
  const handleSyncToAgents = () => {
60
  setIsSyncingToAgents(true);
 
61
  setTimeout(() => {
62
  setIsSyncingToAgents(false);
63
  const firstSelected = tenders.find(t => t.code === selectedCodes[0]);
 
199
  <th className="px-4 py-5 w-[250px]">Opportunity</th>
200
  <th className="px-4 py-5 w-[180px]">Buyer</th>
201
  <th className="px-4 py-5 text-center w-[100px]">Status</th>
 
 
202
  </tr>
203
  </thead>
204
  <tbody className="divide-y divide-white/5">
205
  {filteredTenders.map((tender) => (
206
+ <tr
207
+ key={tender.code}
208
+ className={`hover:bg-white/[0.04] cursor-pointer transition-colors group ${selectedCodes.includes(tender.code) ? 'bg-purple-500/5' : ''}`}
209
+ onClick={() => setSelectedTenderForModal(tender)}
210
+ >
211
+ <td className="px-4 py-5">
212
+ <div className="flex items-center gap-2">
213
+ <input
214
+ type="checkbox"
215
+ checked={selectedCodes.includes(tender.code)}
216
+ onChange={(e) => {
217
+ e.stopPropagation();
218
+ toggleSelect(tender.code);
219
+ }}
220
+ className="w-3.5 h-3.5 rounded border-white/10 bg-white/5 text-purple-500 focus:ring-purple-500/40"
221
+ />
222
+ <button
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  onClick={(e) => {
224
  e.stopPropagation();
225
+ toggleFollow(tender.code);
226
  }}
227
+ 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'}`}
228
  >
229
+ {followedCodes.includes(tender.code) ? "★" : "☆"}
230
  </button>
231
+ <span className="font-mono text-purple-400 text-[9px] truncate">{tender.code}</span>
232
+ </div>
233
+ </td>
234
+ <td className="px-4 py-5">
235
+ <div className="font-semibold text-white group-hover:text-purple-400 transition-colors truncate text-xs">{tender.name}</div>
236
+ <div className="flex items-center gap-2 mt-1">
237
+ <span className="text-[9px] text-slate-500 truncate">{tender.region || "Nacional"}</span>
238
+ <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>
239
+ </div>
240
+ </td>
241
+ <td className="px-4 py-5 text-slate-400 text-[11px] truncate">{tender.buyer}</td>
242
+ <td className="px-4 py-5 text-center">
243
+ <span className={`inline-block rounded-full px-2 py-0.5 text-[9px] font-bold ${
244
+ tender.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400 border border-green-500/20' : 'bg-slate-800/50 text-slate-500'
245
+ }`}>
246
+ {tender.status}
247
+ </span>
248
+ </td>
249
+ </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  ))}
251
  </tbody>
252
  </table>
 
255
  )}
256
  </div>
257
 
258
+ {/* Details Modal */}
259
+ {selectedTenderForModal && (
260
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-10">
261
+ <div className="absolute inset-0 bg-slate-950/80 backdrop-blur-md" onClick={() => setSelectedTenderForModal(null)} />
262
+ <div className="relative w-full max-w-4xl max-h-[90vh] bg-slate-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-300">
263
+ {/* Modal Header */}
264
+ <div className="p-6 md:p-8 border-b border-white/10 flex justify-between items-start">
265
+ <div>
266
+ <div className="flex items-center gap-3 mb-2">
267
+ <span className="text-xs font-mono text-purple-400 bg-purple-400/10 px-2 py-0.5 rounded-md">{selectedTenderForModal.code}</span>
268
+ <span className={`px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-tighter ${
269
+ selectedTenderForModal.status.toLowerCase().includes('publicada') ? 'bg-green-500/10 text-green-400' : 'bg-slate-800 text-slate-500'
270
+ }`}>
271
+ {selectedTenderForModal.status}
272
+ </span>
273
+ </div>
274
+ <h3 className="text-2xl font-bold text-white">{selectedTenderForModal.name}</h3>
275
+ <p className="text-slate-400 text-sm mt-1">{selectedTenderForModal.buyer}</p>
276
+ </div>
277
+ <button
278
+ onClick={() => setSelectedTenderForModal(null)}
279
+ className="p-2 bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white rounded-xl transition-all"
280
+ >
281
+
282
+ </button>
283
+ </div>
284
+
285
+ {/* Modal Content */}
286
+ <div className="flex-1 overflow-y-auto p-6 md:p-8 custom-scrollbar">
287
+ <div className="grid gap-10 md:grid-cols-3">
288
+ {/* Left Column: Main Info */}
289
+ <div className="md:col-span-2 space-y-8">
290
+ <div>
291
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 mb-4 flex items-center gap-2">
292
+ <span className="w-4 h-[1px] bg-slate-700" />
293
+ Project Description
294
+ </h4>
295
+ <p className="text-slate-300 leading-relaxed text-sm bg-slate-800/50 p-6 rounded-2xl border border-white/5 whitespace-pre-wrap">
296
+ {selectedTenderForModal.description || "No detailed description provided."}
297
+ </p>
298
+ </div>
299
+
300
+ <div className="grid grid-cols-2 gap-4">
301
+ <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5">
302
+ <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Estimated Amount</div>
303
+ <div className="text-sm text-white font-bold">
304
+ {selectedTenderForModal.estimated_amount ? new Intl.NumberFormat("es-CL", { style: "currency", currency: "CLP" }).format(selectedTenderForModal.estimated_amount) : "Not Disclosed"}
305
+ </div>
306
+ </div>
307
+ <div className="p-5 rounded-2xl bg-white/[0.03] border border-white/5">
308
+ <div className="text-[9px] uppercase text-slate-500 font-black mb-1">Market Sector</div>
309
+ <div className="text-sm text-white font-bold">{selectedTenderForModal.sector || "General"}</div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+
314
+ {/* Right Column: Meta & Links */}
315
+ <div className="space-y-8">
316
+ <div className="p-6 rounded-2xl bg-purple-500/5 border border-purple-500/10">
317
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-purple-400 mb-4">Deadline</h4>
318
+ <div className="text-xl font-mono text-white mb-1">
319
+ {selectedTenderForModal.closing_date ? new Date(selectedTenderForModal.closing_date).toLocaleDateString() : "---"}
320
+ </div>
321
+ <p className="text-[10px] text-purple-400/60 uppercase">Final day for submission</p>
322
+ </div>
323
+
324
+ <div>
325
+ <h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 mb-4">Official Resources</h4>
326
+ <div className="space-y-3">
327
+ {selectedTenderForModal.attachments?.map((att, i) => (
328
+ <a key={i} href={att.url} target="_blank" className="flex items-center justify-between p-4 rounded-xl bg-slate-800 hover:bg-slate-700 border border-white/5 transition-all group/file">
329
+ <div className="flex items-center gap-3">
330
+ <span className="text-xl">{att.name.endsWith('.pdf') ? "📕" : "📘"}</span>
331
+ <span className="text-xs font-medium text-slate-300 truncate max-w-[120px]">{att.name}</span>
332
+ </div>
333
+ <span className="text-[8px] font-black text-purple-400 opacity-0 group-hover/file:opacity-100 uppercase transition-all tracking-tighter">Download</span>
334
+ </a>
335
+ ))}
336
+ {!selectedTenderForModal.attachments?.length && (
337
+ <p className="text-[10px] text-slate-600 italic">No attachments found.</p>
338
+ )}
339
+ </div>
340
+ </div>
341
+
342
+ <a
343
+ href={`https://www.mercadopublico.cl/fichaLicitacion.html?code=${selectedTenderForModal.code}`}
344
+ target="_blank"
345
+ className="flex items-center justify-center gap-2 w-full p-4 rounded-xl bg-white/5 border border-white/10 text-slate-300 text-xs font-bold hover:bg-white/10 transition"
346
+ >
347
+ Open official portal ↗
348
+ </a>
349
+ </div>
350
+ </div>
351
+ </div>
352
+
353
+ {/* Modal Footer */}
354
+ <div className="p-6 bg-slate-950/50 border-t border-white/10 flex justify-end gap-4">
355
+ <button
356
+ onClick={() => setSelectedTenderForModal(null)}
357
+ className="px-6 py-3 text-sm font-bold text-slate-400 hover:text-white transition"
358
+ >
359
+ Cancel
360
+ </button>
361
+ <button
362
+ onClick={() => {
363
+ onAnalyze(selectedTenderForModal);
364
+ setSelectedTenderForModal(null);
365
+ }}
366
+ className="premium-gradient text-white px-8 py-3 rounded-xl font-bold text-sm shadow-xl shadow-purple-500/20 active:scale-95 transition-all"
367
+ >
368
+ Run Agent Analysis
369
+ </button>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ )}
374
+
375
  {isLoading && <BrandLoader />}
376
  </div>
377
  );