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
+25 -2
View File
@@ -34,15 +34,38 @@ export function firmStats(tenders) {
awardCount++;
const name = t.kazananFirma.trim();
const val = parseTL(t.sozlesmeBedeli);
const cur = map.get(name) || { name, wins: 0, total: 0, sectors: new Set(), cities: new Set() };
const cur = map.get(name) || {
name, wins: 0, total: 0, sectors: new Set(), cities: new Set(),
// Kazanan istihbaratı: kırım oranı (yaklaşık maliyete göre ne kadar
// aşağı verdi) ve rekabet derinliği (kaç istekliye karşı kazandı)
kirimToplam: 0, kirimAdet: 0, teklifToplam: 0, teklifAdet: 0, tekTeklif: 0,
};
cur.wins++;
if (val != null) cur.total += val;
const maliyet = parseTL(t.yaklasikMaliyet);
if (val != null && maliyet > 0 && val <= maliyet * 1.5) {
cur.kirimToplam += 1 - val / maliyet;
cur.kirimAdet++;
}
const teklif = Number(t.gecerliTeklifSayisi ?? t.teklifSayisi);
if (Number.isFinite(teklif) && teklif > 0) {
cur.teklifToplam += teklif;
cur.teklifAdet++;
if (teklif === 1) cur.tekTeklif++;
}
if (t.sektor) cur.sectors.add(t.sektor);
if (t.sehir) cur.cities.add(t.sehir);
map.set(name, cur);
}
const firms = [...map.values()]
.map((f) => ({ ...f, sectors: [...f.sectors], cities: [...f.cities] }))
.map((f) => ({
...f,
sectors: [...f.sectors], cities: [...f.cities],
// yüzde; veri yoksa null — 0 ile karıştırılmasın
kirimOrani: f.kirimAdet ? Math.round((f.kirimToplam / f.kirimAdet) * 100) : null,
ortTeklif: f.teklifAdet ? Math.round((f.teklifToplam / f.teklifAdet) * 10) / 10 : null,
tekTeklifOrani: f.teklifAdet ? Math.round((f.tekTeklif / f.teklifAdet) * 100) : null,
}))
.sort((a, b) => b.wins - a.wins || b.total - a.total);
const totalMarket = firms.reduce((s, f) => s + f.total, 0);
return {