Á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
|
| 100 |
if complaints_match:
|
| 101 |
result["metadata"]["buyer_complaints"] = int(complaints_match.group(1))
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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/
|
| 246 |
target="_blank"
|
| 247 |
rel="noopener noreferrer"
|
| 248 |
-
className="
|
| 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>
|