feat: OKAS kod aramasi, toplu ihale dokumani yukleme, kazanan istihbarati

Uc ozellik, 27+1 depo taramasinda gorulen fikirlerden; kod kopyalanmadi
(ilgili depolar lisanssiz), uygulamalar sifirdan yazildi.

1) OKAS koduyla arama (fikir: firatbadur/ihaletakip-api'nin kullandigi
   IhtiyacKalemleri/GetAll ucu). EKAP'in kendi OKAS katalogunda ad ile
   (TR/EN) arama: "ortoped" → 33141700 Ortopedik malzemeler, 33183000
   Ortopedik destek cihazlari. Secilen kodla EKAP'ta ihale aramasi
   (okasBransKodList). Olculen: 33141700 altinda 335 ihale, basliklari
   "VETERINER ILAC VE SARF MALZEME" — basliktan gorunmeyen turden.
   - ekap-v2.mjs: okasAra(), searchByOkas(); sonuc normalize ortak
   - GET /api/okas/ara?q= (1 sa onbellek); /api/urun-ara?okas=kod,kod
   - Urun Arama modalinda "OKAS koduyla" modu (katalog onerisi + cok kod)
   - Sol menude CPV bolumunun ustunde OKAS kutusu: kod secilince modal
     o kodla acilip hemen arar

2) Toplu ihale dokumani yukleme (fikir: barkem-tayyar/ihale'nin izlenen
   klasor + extractIkn deseni). Belge Kasasi'na coklu dosya secimi: her
   dosyanin IKN'si METNINDEN bulunur ("Ihale Kayit Numarasi (IKN):
   2026/1166496"), kullaniciya sorulmaz; ihaleye baglanir ve teklif
   cetveli kalemleri otomatik havuza yazilir. Onlarca sartnameyi tek
   seferde atarak kalem havuzunu hizla doldurmak icin.
   - kalem-cikar.mjs#iknCikar + istemci esi (PDF tarayicida okunuyor)
   - Yukleme ucu: otoIkn → ofis metninden IKN; donuste ikn alani
   - Dosya basina sonuc satiri: IKN / kalem sayisi / "IKN bulunamadi"
   Dogrulandi: ikn parametresiz docx → 2026/1588948 bulundu, 2 kalem.

3) Kazanan istihbarati (fikir: gulmezeren2-byte/ihalent, MIT). Rakip
   Radari'nda firma basina kirim orani (1 − sozlesme/yaklasik maliyet,
   ortalama), ortalama gecerli teklif sayisi (rekabet derinligi) ve tek
   teklifle kazanma orani. Veri yoksa null — 0 ile karismasin; bedel >
   maliyet×1,5 olan bozuk satirlar orana katilmiyor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:16:53 +03:00
co-authored by Claude Fable 5.1
parent 9c142fa0f8
commit 1fce9df06f
12 changed files with 406 additions and 36 deletions
+40 -14
View File
@@ -167,6 +167,45 @@ export async function searchTenders(opts = {}) {
* ve o ihalelerin adı "VETERİNER İLAÇ ... SARF MALZEME". EKAP dokümanı
* kendi tarafında indekslediği için doküman indirmeye gerek yok.
*/
/**
* OKAS kataloğunda ad ile arama (TR/EN). Kaynak: EKAP'ın kendi kalem servisi,
* DevExtreme loadOptions gövdesiyle. "ortoped" → 33141700 Ortopedik malzemeler.
*/
export async function okasAra(q, { take = 30 } = {}) {
const terim = String(q || '').trim();
const filter = terim ? [['kalemAdi', 'contains', terim], 'or', ['kalemAdiEng', 'contains', terim]] : [];
const d = await ekapPost('/b_ihalearama/api/IhtiyacKalemleri/GetAll', {
loadOptions: {
filter: { sort: [], group: [], filter, totalSummary: [], groupSummary: [], select: [], preSelect: [], primaryKey: [] },
take,
},
});
const liste = d?.loadResult?.data || [];
return liste.map((k) => ({
kod: k.kod, ad: (k.kalemAdi || '').trim(), adEn: (k.kalemAdiEng || '').trim(),
seviye: k.kodLevel, altSayisi: k.childCount ?? 0,
}));
}
function normalizeKayit(t) {
return {
ikn: t.ikn, baslik: t.ihaleAdi, kurum: t.idareAdi, sehir: t.ihaleIlAdi,
tur: t.ihaleTipAciklama, usul: t.ihaleUsulAciklama, durum: t.ihaleDurumAciklama,
ihaleTarihSaat: t.ihaleTarihSaat, dokumanSayisi: t.dokumanSayisi ?? 0,
};
}
/** OKAS kodlarıyla EKAP araması — idarenin kendi sınıflandırmasına göre. */
export async function searchByOkas(kodlar, { skip = 0, take = 20 } = {}) {
const liste = (Array.isArray(kodlar) ? kodlar : [kodlar]).map(String).filter(Boolean);
if (!liste.length) return { totalCount: 0, list: [] };
const data = await ekapPost(
'/b_ihalearama/api/Ihale/GetListByParameters',
searchBody({ okasBransKodList: liste, skip, take }),
);
return { totalCount: data.totalCount ?? 0, list: (data.list || []).map(normalizeKayit) };
}
export async function searchInDocuments(metin, { teknikSartname = false, skip = 0, take = 20 } = {}) {
const kapsam = {
ikNdeAra: false, ihaleAdindaAra: false, ihaleIlanindaAra: false,
@@ -179,20 +218,7 @@ export async function searchInDocuments(metin, { teknikSartname = false, skip =
'/b_ihalearama/api/Ihale/GetListByParameters',
searchBody({ searchText: metin, kapsam, skip, take }),
);
return {
totalCount: data.totalCount ?? 0,
list: (data.list || []).map((t) => ({
ikn: t.ikn,
baslik: t.ihaleAdi,
kurum: t.idareAdi,
sehir: t.ihaleIlAdi,
tur: t.ihaleTipAciklama,
usul: t.ihaleUsulAciklama,
durum: t.ihaleDurumAciklama,
ihaleTarihSaat: t.ihaleTarihSaat,
dokumanSayisi: t.dokumanSayisi ?? 0,
})),
};
return { totalCount: data.totalCount ?? 0, list: (data.list || []).map(normalizeKayit) };
}
// NOT: dönen `id` per-response şifreli token — kalıcı değil, aynı oturumda kullan.