Á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 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
- return await get_tenders_by_provider(provider_code, date)
43
-
44
- if org_code:
45
- return await get_tenders_by_org(org_code, date)
46
-
47
- if status:
48
- return await get_tenders_by_status_and_date(status, date)
49
-
50
- if date and not (buyer or region or keyword):
51
- return await get_tenders_by_date(date)
52
 
53
- if keyword and not (buyer or region):
54
- return await fetch_tenders(keyword=keyword, date=date)
 
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 get_active_tenders() -> List[Tender]:
174
- """Shows all tenders published on the day of the query."""
175
- return await _fetch({"estado": "activas"})
176
-
177
- async def get_tenders_by_date(date: str) -> List[Tender]:
178
- """Fetches all tenders for a specific date (format ddmmaaaa)."""
179
- normalized = normalize_mp_date(date)
180
- return await _fetch({"fecha": normalized})
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
- return await _fetch(params)
187
-
188
- async def get_tender_by_code(code: str) -> Optional[Tender]:
189
- results = await _fetch({"codigo": code})
190
- return results[0] if results else None
191
-
192
- async def get_tenders_by_provider(provider_code: str, date: Optional[str] = None) -> List[Tender]:
193
- normalized_date = normalize_mp_date(date if date else datetime.now().strftime("%Y-%m-%d"))
194
- return await _fetch({"CodigoProveedor": provider_code, "fecha": normalized_date})
195
-
196
- async def get_tenders_by_org(org_code: str, date: Optional[str] = None) -> List[Tender]:
197
- normalized_date = normalize_mp_date(date if date else datetime.now().strftime("%Y-%m-%d"))
198
- return await _fetch({"CodigoOrganismo": org_code, "fecha": normalized_date})
 
 
 
 
 
 
 
 
 
 
199
 
200
- async def fetch_tenders(keyword: Optional[str] = None, date: Optional[str] = None) -> List[Tender]:
 
 
 
 
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">activas</option>
255
- <option value="publicada">publicada</option>
256
- <option value="cerrada">cerrada</option>
257
- <option value="desierta">desierta</option>
258
- <option value="adjudicada">adjudicada</option>
259
- <option value="revocada">revocada</option>
260
- <option value="suspendida">suspendida</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <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 <span className="font-semibold text-white">Provider Code</span>, <span className="font-semibold text-white">Org Code</span>, <span className="font-semibold text-white">Status</span> y <span className="font-semibold text-white">Date</span> para consultas directas en Mercado Público.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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