Álvaro Valenzuela Valdes commited on
Commit ·
956c5af
1
Parent(s): 7a29176
feat: enhance advanced search with Date, Org, and Type filters using MP API
Browse files- backend/app/routers/tenders.py +13 -13
- backend/app/services/mercado_publico.py +40 -25
- frontend/app/page.tsx +1 -1
- frontend/components/TenderSearch.tsx +47 -10
- frontend/lib/api.ts +2 -0
backend/app/routers/tenders.py
CHANGED
|
@@ -29,6 +29,7 @@ async def search_tender_opportunities(
|
|
| 29 |
status: Optional[str] = None,
|
| 30 |
code: Optional[str] = None,
|
| 31 |
date: Optional[str] = None,
|
|
|
|
| 32 |
skip: int = 0,
|
| 33 |
limit: int = 50,
|
| 34 |
db: Session = Depends(get_db)
|
|
@@ -38,20 +39,19 @@ async def search_tender_opportunities(
|
|
| 38 |
tender = await get_tender_by_code(code)
|
| 39 |
return [tender] if tender else []
|
| 40 |
|
| 41 |
-
if provider_code:
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
if date and not (buyer or region or keyword):
|
| 51 |
-
return await get_tenders_by_date(date)
|
| 52 |
|
| 53 |
-
if keyword
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
# 1. Búsqueda en DB con paginación
|
| 57 |
query = db.query(TenderModel)
|
|
|
|
| 29 |
status: Optional[str] = None,
|
| 30 |
code: Optional[str] = None,
|
| 31 |
date: Optional[str] = None,
|
| 32 |
+
type_code: Optional[str] = Query(None, alias="type_code"),
|
| 33 |
skip: int = 0,
|
| 34 |
limit: int = 50,
|
| 35 |
db: Session = Depends(get_db)
|
|
|
|
| 39 |
tender = await get_tender_by_code(code)
|
| 40 |
return [tender] if tender else []
|
| 41 |
|
| 42 |
+
if any([provider_code, org_code, status, date, type_code]) and not keyword:
|
| 43 |
+
from app.services.mercado_publico import get_tenders_by_filters
|
| 44 |
+
return await get_tenders_by_filters(
|
| 45 |
+
date=date,
|
| 46 |
+
status=status,
|
| 47 |
+
type_code=type_code,
|
| 48 |
+
org_code=org_code,
|
| 49 |
+
provider_code=provider_code
|
| 50 |
+
)
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
if keyword:
|
| 53 |
+
from app.services.mercado_publico import fetch_tenders
|
| 54 |
+
return await fetch_tenders(keyword=keyword, date=date, type_code=type_code)
|
| 55 |
|
| 56 |
# 1. Búsqueda en DB con paginación
|
| 57 |
query = db.query(TenderModel)
|
backend/app/services/mercado_publico.py
CHANGED
|
@@ -170,34 +170,45 @@ async def _fetch(params: Dict[str, str], retries: int = 3) -> List[Tender]:
|
|
| 170 |
return []
|
| 171 |
return []
|
| 172 |
|
| 173 |
-
async def
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
async def get_tenders_by_status_and_date(status: str, date: Optional[str] = None) -> List[Tender]:
|
| 183 |
-
params = {"estado": status}
|
| 184 |
if date:
|
| 185 |
params["fecha"] = normalize_mp_date(date)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
async def fetch_tenders(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
search_date = normalize_mp_date(date if date else datetime.now().strftime("%Y-%m-%d"))
|
| 202 |
|
| 203 |
if not date:
|
|
@@ -205,6 +216,10 @@ async def fetch_tenders(keyword: Optional[str] = None, date: Optional[str] = Non
|
|
| 205 |
else:
|
| 206 |
tenders = await get_tenders_by_date(search_date)
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
if keyword:
|
| 209 |
keyword = keyword.lower()
|
| 210 |
tenders = [t for t in tenders if keyword in t.name.lower() or keyword in t.description.lower()]
|
|
|
|
| 170 |
return []
|
| 171 |
return []
|
| 172 |
|
| 173 |
+
async def get_tenders_by_filters(
|
| 174 |
+
date: Optional[str] = None,
|
| 175 |
+
status: Optional[str] = None,
|
| 176 |
+
type_code: Optional[str] = None,
|
| 177 |
+
org_code: Optional[str] = None,
|
| 178 |
+
provider_code: Optional[str] = None
|
| 179 |
+
) -> List[Tender]:
|
| 180 |
+
params = {}
|
|
|
|
|
|
|
|
|
|
| 181 |
if date:
|
| 182 |
params["fecha"] = normalize_mp_date(date)
|
| 183 |
+
else:
|
| 184 |
+
# Default to today if no date is provided for specific filters
|
| 185 |
+
if status or org_code or provider_code:
|
| 186 |
+
params["fecha"] = datetime.now().strftime("%d%m%Y")
|
| 187 |
+
|
| 188 |
+
if status:
|
| 189 |
+
params["estado"] = status
|
| 190 |
+
if org_code:
|
| 191 |
+
params["CodigoOrganismo"] = org_code
|
| 192 |
+
if provider_code:
|
| 193 |
+
params["CodigoProveedor"] = provider_code
|
| 194 |
+
|
| 195 |
+
# If no specific filter and no date, default to active
|
| 196 |
+
if not params:
|
| 197 |
+
return await get_active_tenders()
|
| 198 |
+
|
| 199 |
+
tenders = await _fetch(params)
|
| 200 |
+
|
| 201 |
+
if type_code:
|
| 202 |
+
type_code = type_code.upper()
|
| 203 |
+
tenders = [t for t in tenders if t.raw_data.get("CodigoTipo") == type_code or type_code in (t.type or "")]
|
| 204 |
+
|
| 205 |
+
return tenders
|
| 206 |
|
| 207 |
+
async def fetch_tenders(
|
| 208 |
+
keyword: Optional[str] = None,
|
| 209 |
+
date: Optional[str] = None,
|
| 210 |
+
type_code: Optional[str] = None
|
| 211 |
+
) -> List[Tender]:
|
| 212 |
search_date = normalize_mp_date(date if date else datetime.now().strftime("%Y-%m-%d"))
|
| 213 |
|
| 214 |
if not date:
|
|
|
|
| 216 |
else:
|
| 217 |
tenders = await get_tenders_by_date(search_date)
|
| 218 |
|
| 219 |
+
if type_code:
|
| 220 |
+
type_code = type_code.upper()
|
| 221 |
+
tenders = [t for t in tenders if t.raw_data.get("CodigoTipo") == type_code or type_code in (t.type or "")]
|
| 222 |
+
|
| 223 |
if keyword:
|
| 224 |
keyword = keyword.lower()
|
| 225 |
tenders = [t for t in tenders if keyword in t.name.lower() or keyword in t.description.lower()]
|
frontend/app/page.tsx
CHANGED
|
@@ -129,7 +129,7 @@ export default function HomePage() {
|
|
| 129 |
window.history.pushState({}, '', `?tab=tender_search&q=${encodeURIComponent(value)}`);
|
| 130 |
};
|
| 131 |
|
| 132 |
-
const handleSearch = async (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; skip?: number; limit?: number; isAgile?: boolean }) => {
|
| 133 |
try {
|
| 134 |
let results: Tender[];
|
| 135 |
if (params.isAgile && params.keyword) {
|
|
|
|
| 129 |
window.history.pushState({}, '', `?tab=tender_search&q=${encodeURIComponent(value)}`);
|
| 130 |
};
|
| 131 |
|
| 132 |
+
const handleSearch = async (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; type_code?: string; skip?: number; limit?: number; isAgile?: boolean }) => {
|
| 133 |
try {
|
| 134 |
let results: Tender[];
|
| 135 |
if (params.isAgile && params.keyword) {
|
frontend/components/TenderSearch.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import type { CompanyProfile } from "../lib/types";
|
|
| 9 |
|
| 10 |
type Props = {
|
| 11 |
tenders: Tender[];
|
| 12 |
-
onSearch: (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; skip?: number; limit?: number; isAgile?: boolean }) => void;
|
| 13 |
onAnalyze: (tender: Tender) => void;
|
| 14 |
forceShowFollowed?: boolean;
|
| 15 |
initialKeyword?: string;
|
|
@@ -25,6 +25,8 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 25 |
const [orgCode, setOrgCode] = useState("");
|
| 26 |
const [status, setStatus] = useState("");
|
| 27 |
const [date, setDate] = useState("");
|
|
|
|
|
|
|
| 28 |
const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
|
| 29 |
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
| 30 |
const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
|
|
@@ -128,6 +130,7 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 128 |
provider_code: providerCode || undefined,
|
| 129 |
org_code: orgCode || undefined,
|
| 130 |
status: status || undefined,
|
|
|
|
| 131 |
buyer: buyerCode,
|
| 132 |
date,
|
| 133 |
skip: 0,
|
|
@@ -251,13 +254,30 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 251 |
onChange={(e) => setStatus(e.target.value)}
|
| 252 |
>
|
| 253 |
<option value="">Any state</option>
|
| 254 |
-
<option value="activas">
|
| 255 |
-
<option value="
|
| 256 |
-
<option value="
|
| 257 |
-
<option value="
|
| 258 |
-
<option value="
|
| 259 |
-
<option value="
|
| 260 |
-
<option value="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
</select>
|
| 262 |
</div>
|
| 263 |
<div className="space-y-2">
|
|
@@ -270,8 +290,25 @@ export default function TenderSearch({ tenders, onSearch, onAnalyze, forceShowFo
|
|
| 270 |
/>
|
| 271 |
</div>
|
| 272 |
</div>
|
| 273 |
-
<div className="rounded-3xl bg-slate-900/70 border border-white/10 p-4 text-slate-400 text-xs leading-5">
|
| 274 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
</div>
|
| 276 |
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
| 277 |
<div className="flex flex-col gap-2">
|
|
|
|
| 9 |
|
| 10 |
type Props = {
|
| 11 |
tenders: Tender[];
|
| 12 |
+
onSearch: (params: { keyword?: string; buyer?: string; provider_code?: string; org_code?: string; status?: string; code?: string; date?: string; type_code?: string; skip?: number; limit?: number; isAgile?: boolean }) => void;
|
| 13 |
onAnalyze: (tender: Tender) => void;
|
| 14 |
forceShowFollowed?: boolean;
|
| 15 |
initialKeyword?: string;
|
|
|
|
| 25 |
const [orgCode, setOrgCode] = useState("");
|
| 26 |
const [status, setStatus] = useState("");
|
| 27 |
const [date, setDate] = useState("");
|
| 28 |
+
const [typeCode, setTypeCode] = useState("");
|
| 29 |
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
| 30 |
const [selectedTenderForModal, setSelectedTenderForModal] = useState<Tender | null>(null);
|
| 31 |
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
|
| 32 |
const [isSyncingToAgents, setIsSyncingToAgents] = useState(false);
|
|
|
|
| 130 |
provider_code: providerCode || undefined,
|
| 131 |
org_code: orgCode || undefined,
|
| 132 |
status: status || undefined,
|
| 133 |
+
type_code: typeCode || undefined,
|
| 134 |
buyer: buyerCode,
|
| 135 |
date,
|
| 136 |
skip: 0,
|
|
|
|
| 254 |
onChange={(e) => setStatus(e.target.value)}
|
| 255 |
>
|
| 256 |
<option value="">Any state</option>
|
| 257 |
+
<option value="activas">Activas (Today)</option>
|
| 258 |
+
<option value="5">Publicada (5)</option>
|
| 259 |
+
<option value="6">Cerrada (6)</option>
|
| 260 |
+
<option value="7">Desierta (7)</option>
|
| 261 |
+
<option value="8">Adjudicada (8)</option>
|
| 262 |
+
<option value="18">Revocada (18)</option>
|
| 263 |
+
<option value="19">Suspendida (19)</option>
|
| 264 |
+
</select>
|
| 265 |
+
</div>
|
| 266 |
+
<div className="space-y-2">
|
| 267 |
+
<label className="text-[10px] uppercase tracking-wider text-slate-500 font-bold px-1">Tender Type</label>
|
| 268 |
+
<select
|
| 269 |
+
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all"
|
| 270 |
+
value={typeCode}
|
| 271 |
+
onChange={(e) => setTypeCode(e.target.value)}
|
| 272 |
+
>
|
| 273 |
+
<option value="">Any type</option>
|
| 274 |
+
<option value="L1">L1 - Licitación Pública < 100 UTM</option>
|
| 275 |
+
<option value="LE">LE - Licitación Pública 100-1000 UTM</option>
|
| 276 |
+
<option value="LP">LP - Licitación Pública > 1000 UTM</option>
|
| 277 |
+
<option value="LS">LS - Servicios Personales</option>
|
| 278 |
+
<option value="A1">A1 - Privada por Desierta</option>
|
| 279 |
+
<option value="D1">D1 - Trato Directo Proveedor Único</option>
|
| 280 |
+
<option value="C2">C2 - Trato Directo (Cotización)</option>
|
| 281 |
</select>
|
| 282 |
</div>
|
| 283 |
<div className="space-y-2">
|
|
|
|
| 290 |
/>
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
+
<div className="rounded-3xl bg-slate-900/70 border border-white/10 p-4 text-slate-400 text-xs leading-5 flex justify-between items-center">
|
| 294 |
+
<div>
|
| 295 |
+
<span className="font-bold text-slate-200">Tip:</span> Deja todos los campos vacíos para mostrar las licitaciones activas del día. Usa <span className="font-semibold text-white">Tender Code</span> para una búsqueda exacta o los filtros avanzados para consultas directas en Mercado Público.
|
| 296 |
+
</div>
|
| 297 |
+
<button
|
| 298 |
+
type="button"
|
| 299 |
+
onClick={() => {
|
| 300 |
+
setKeyword("");
|
| 301 |
+
setBuyerCode("");
|
| 302 |
+
setProviderCode("");
|
| 303 |
+
setOrgCode("");
|
| 304 |
+
setStatus("");
|
| 305 |
+
setDate("");
|
| 306 |
+
setTypeCode("");
|
| 307 |
+
}}
|
| 308 |
+
className="text-[10px] font-bold uppercase text-slate-500 hover:text-white transition"
|
| 309 |
+
>
|
| 310 |
+
Clear All
|
| 311 |
+
</button>
|
| 312 |
</div>
|
| 313 |
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
| 314 |
<div className="flex flex-col gap-2">
|
frontend/lib/api.ts
CHANGED
|
@@ -28,6 +28,7 @@ export async function searchTenders(params: {
|
|
| 28 |
status?: string;
|
| 29 |
code?: string;
|
| 30 |
date?: string;
|
|
|
|
| 31 |
skip?: number;
|
| 32 |
limit?: number;
|
| 33 |
}): Promise<Tender[]> {
|
|
@@ -39,6 +40,7 @@ export async function searchTenders(params: {
|
|
| 39 |
if (params.status) query.append("status", params.status);
|
| 40 |
if (params.code) query.append("code", params.code);
|
| 41 |
if (params.date) query.append("date", params.date);
|
|
|
|
| 42 |
if (params.skip !== undefined) query.append("skip", params.skip.toString());
|
| 43 |
if (params.limit !== undefined) query.append("limit", params.limit.toString());
|
| 44 |
|
|
|
|
| 28 |
status?: string;
|
| 29 |
code?: string;
|
| 30 |
date?: string;
|
| 31 |
+
type_code?: string;
|
| 32 |
skip?: number;
|
| 33 |
limit?: number;
|
| 34 |
}): Promise<Tender[]> {
|
|
|
|
| 40 |
if (params.status) query.append("status", params.status);
|
| 41 |
if (params.code) query.append("code", params.code);
|
| 42 |
if (params.date) query.append("date", params.date);
|
| 43 |
+
if (params.type_code) query.append("type_code", params.type_code);
|
| 44 |
if (params.skip !== undefined) query.append("skip", params.skip.toString());
|
| 45 |
if (params.limit !== undefined) query.append("limit", params.limit.toString());
|
| 46 |
|