Álvaro Valenzuela Valdes commited on
Commit
0d90d39
·
1 Parent(s): 46928cd

feat: enhance tender detail scraping with guarantees, items and direct links to portal technical sheet

Browse files
backend/app/services/tender_detail_extractor.py CHANGED
@@ -96,14 +96,32 @@ async def extract_tender_detail_tabs(tender_code: str, qs_param: Optional[str] =
96
  result["metadata"]["has_adjudication"] = True
97
 
98
  # Extract complaints and purchases (New Intelligence)
99
- complaints_match = re.search(r'Reclamos.*?(\d+)', html, re.IGNORECASE)
100
  if complaints_match:
101
  result["metadata"]["buyer_complaints"] = int(complaints_match.group(1))
102
 
103
- purchases_match = re.search(r'compras efectuadas.*?(\d+)', html, re.IGNORECASE)
104
- if purchases_match:
105
- result["metadata"]["buyer_purchases"] = int(purchases_match.group(1))
 
 
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  return result
108
 
109
  except Exception as e:
 
96
  result["metadata"]["has_adjudication"] = True
97
 
98
  # Extract complaints and purchases (New Intelligence)
99
+ complaints_match = re.search(r'Reclamos recibidos por incumplir plazo de pago:\s*(\d+)', html, re.IGNORECASE)
100
  if complaints_match:
101
  result["metadata"]["buyer_complaints"] = int(complaints_match.group(1))
102
 
103
+ # Extract Guarantees (Seriedad y Fiel Cumplimiento)
104
+ guarantees = []
105
+ seriedad_match = re.search(r'Garantías de Seriedad de Ofertas.*?Monto:\s*(.*?)(?=<br|</td>|Beneficiario)', html, re.IGNORECASE | re.DOTALL)
106
+ if seriedad_match:
107
+ guarantees.append({"type": "Seriedad de Oferta", "amount": seriedad_match.group(1).strip()})
108
 
109
+ fiel_match = re.search(r'Garantía fiel de Cumplimiento de Contrato.*?Monto:\s*(.*?)(?=<br|</td>|Beneficiario)', html, re.IGNORECASE | re.DOTALL)
110
+ if fiel_match:
111
+ guarantees.append({"type": "Fiel Cumplimiento", "amount": fiel_match.group(1).strip()})
112
+
113
+ result["metadata"]["guarantees"] = guarantees
114
+
115
+ # Extract Detailed Items (Lines)
116
+ items = []
117
+ # Find rows with product codes and descriptions
118
+ item_matches = re.finditer(r'Cod:\s*(\d+).*?</td>.*?<td>\s*(.*?)\s*</td>', html, re.IGNORECASE | re.DOTALL)
119
+ for m in item_matches:
120
+ items.append({"code": m.group(1), "description": m.group(2).strip()})
121
+
122
+ if items:
123
+ result["metadata"]["detailed_items"] = items
124
+
125
  return result
126
 
127
  except Exception as e:
frontend/components/AgentAnalysis.tsx CHANGED
@@ -356,6 +356,21 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
356
  </div>
357
  )}
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  {tender?.description && (
360
  <div className="mt-8 p-6 rounded-2xl bg-white/[0.02] border border-white/5">
361
  <h4 className="text-[10px] font-bold uppercase text-slate-500 mb-3 tracking-[0.2em]">Detailed Scope</h4>
@@ -393,6 +408,23 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
393
  </div>
394
  )}
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  {/* Scraped Intelligence / Tabs */}
397
  {tenderDetails && (
398
  <div className="mt-8 flex flex-wrap gap-3">
@@ -674,12 +706,19 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
674
  <span className="text-[10px] text-slate-500 font-mono">Analyzing:</span>
675
  <span className="text-[10px] text-purple-300 font-bold truncate max-w-[200px]">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span>
676
  </div>
677
- </div>
678
- <div className="flex flex-col items-end gap-3 w-full sm:w-auto">
679
- <div className={`w-full sm:w-auto text-center rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${activeAnalysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
680
  {activeAnalysis.decision}
681
  </div>
682
  <div className="flex flex-wrap gap-2 w-full sm:w-auto justify-end">
 
 
 
 
 
 
 
 
 
683
  <button
684
  onClick={() => window.print()}
685
  className="flex-1 sm:flex-none px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest"
 
356
  </div>
357
  )}
358
 
359
+ {/* Guarantees Section */}
360
+ {tenderDetails?.metadata?.guarantees && tenderDetails.metadata.guarantees.length > 0 && (
361
+ <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
362
+ {tenderDetails.metadata.guarantees.map((g: any, i: number) => (
363
+ <div key={i} className="rounded-2xl bg-amber-500/5 border border-amber-500/20 p-4 flex items-center justify-between">
364
+ <div>
365
+ <p className="text-[9px] uppercase text-amber-500/60 font-black tracking-[0.2em] mb-1">{g.type}</p>
366
+ <p className="text-sm font-bold text-white">{g.amount}</p>
367
+ </div>
368
+ <div className="text-xl">🛡️</div>
369
+ </div>
370
+ ))}
371
+ </div>
372
+ )}
373
+
374
  {tender?.description && (
375
  <div className="mt-8 p-6 rounded-2xl bg-white/[0.02] border border-white/5">
376
  <h4 className="text-[10px] font-bold uppercase text-slate-500 mb-3 tracking-[0.2em]">Detailed Scope</h4>
 
408
  </div>
409
  )}
410
 
411
+ {/* Detailed Scraped Items */}
412
+ {tenderDetails?.metadata?.detailed_items && tenderDetails.metadata.detailed_items.length > 0 && (
413
+ <div className="mt-8 overflow-hidden rounded-2xl border border-purple-500/20 bg-purple-500/5">
414
+ <div className="bg-purple-500/10 px-6 py-3 border-b border-purple-500/20">
415
+ <h4 className="text-[10px] font-black uppercase text-purple-300 tracking-widest">Portal Line Items Intelligence</h4>
416
+ </div>
417
+ <div className="p-4 space-y-3 max-h-60 overflow-y-auto custom-scrollbar">
418
+ {tenderDetails.metadata.detailed_items.map((item: any, idx: number) => (
419
+ <div key={idx} className="flex gap-4 items-start p-3 rounded-xl bg-white/5 border border-white/5 hover:border-purple-500/30 transition-all">
420
+ <span className="bg-purple-500/20 text-purple-400 px-2 py-1 rounded text-[9px] font-mono font-bold shrink-0">{item.code}</span>
421
+ <p className="text-[11px] text-slate-300 leading-relaxed italic">"{item.description}"</p>
422
+ </div>
423
+ ))}
424
+ </div>
425
+ </div>
426
+ )}
427
+
428
  {/* Scraped Intelligence / Tabs */}
429
  {tenderDetails && (
430
  <div className="mt-8 flex flex-wrap gap-3">
 
706
  <span className="text-[10px] text-slate-500 font-mono">Analyzing:</span>
707
  <span className="text-[10px] text-purple-300 font-bold truncate max-w-[200px]">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span>
708
  </div>
709
+ className={`w-full sm:w-auto text-center rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${activeAnalysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
 
 
710
  {activeAnalysis.decision}
711
  </div>
712
  <div className="flex flex-wrap gap-2 w-full sm:w-auto justify-end">
713
+ <a
714
+ href={`https://www.mercadopublico.cl/Procurement/Modules/RFB/DetailsAcquisition.aspx?codigo=${tender?.code}`}
715
+ target="_blank"
716
+ rel="noopener noreferrer"
717
+ className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest group"
718
+ >
719
+ <span>Visit Official Site</span>
720
+ <span className="group-hover:translate-x-1 transition-transform">🔗</span>
721
+ </a>
722
  <button
723
  onClick={() => window.print()}
724
  className="flex-1 sm:flex-none px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[9px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest"
frontend/components/TenderSearch.tsx CHANGED
@@ -242,10 +242,10 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
242
  <div className="glass-card rounded-[2.5rem] overflow-hidden border border-white/5 bg-slate-900/40 backdrop-blur-xl p-10 md:p-14 relative">
243
  <div className="absolute top-0 right-0 p-8">
244
  <a
245
- href={`https://www.mercadopublico.cl/Portal/BuscarLicitacion?Texto=${encodeURIComponent(tender.code)}`}
246
  target="_blank"
247
  rel="noopener noreferrer"
248
- className="flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan/10 border border-cyan/30 text-cyan text-[10px] font-black uppercase tracking-widest hover:bg-cyan/20 transition-all"
249
  >
250
  Visit Official Site 🔗
251
  </a>
 
242
  <div className="glass-card rounded-[2.5rem] overflow-hidden border border-white/5 bg-slate-900/40 backdrop-blur-xl p-10 md:p-14 relative">
243
  <div className="absolute top-0 right-0 p-8">
244
  <a
245
+ href={`https://www.mercadopublico.cl/Procurement/Modules/RFB/DetailsAcquisition.aspx?codigo=${tender.code}`}
246
  target="_blank"
247
  rel="noopener noreferrer"
248
+ className="px-4 py-2 rounded-xl bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-400 hover:bg-purple-500/20 transition-all uppercase tracking-widest whitespace-nowrap"
249
  >
250
  Visit Official Site 🔗
251
  </a>