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:
@@ -44,7 +44,7 @@ export const revokeApiKey = (id) => iste(`/api/me/apikeys?id=${encodeURIComponen
|
||||
export const listDocuments = () => iste('/api/me/documents');
|
||||
export const deleteDocument = (id) => iste(`/api/me/documents?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
|
||||
export async function uploadDocument(file, kategori, ikn) {
|
||||
export async function uploadDocument(file, kategori, ikn, ek = {}) {
|
||||
const icerikBase64 = await new Promise((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => resolve(String(r.result).split(',')[1]);
|
||||
@@ -53,7 +53,7 @@ export async function uploadDocument(file, kategori, ikn) {
|
||||
});
|
||||
return iste('/api/me/documents', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ad: file.name, mime: file.type, kategori, ikn, icerikBase64 }),
|
||||
body: JSON.stringify({ ad: file.name, mime: file.type, kategori, ikn, icerikBase64, ...ek }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -37,3 +37,23 @@ export async function kalemCikar({ ikn, metin, dosya }) {
|
||||
if (!d.success) throw new Error(d.error || 'kalem çıkarılamadı');
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doküman metninden İKN — toplu yüklemede PDF tarayıcıda okunduğu için
|
||||
* sunucudaki kalem-cikar.mjs#iknCikar'ın istemci eşi.
|
||||
*/
|
||||
export function iknCikar(metin) {
|
||||
const duz = String(metin || '').slice(0, 20000).toLocaleLowerCase('tr-TR')
|
||||
.replace(/ı/g, 'i').replace(/ş/g, 's').replace(/ğ/g, 'g').replace(/ç/g, 'c').replace(/ö/g, 'o').replace(/ü/g, 'u');
|
||||
const m = /ihale\s+kayit\s+numarasi\s*(?:\(\s*ikn\s*\))?\s*:?\s*(\d{4}[/-]\d{3,8})/.exec(duz)
|
||||
|| /\bikn\s*[:=]?\s*(\d{4}[/-]\d{3,8})/.exec(duz);
|
||||
return m ? m[1].replace('-', '/') : null;
|
||||
}
|
||||
|
||||
/** OKAS kataloğunda ad ile arama — "ortoped" → 33141700 Ortopedik malzemeler */
|
||||
export async function okasAra(q) {
|
||||
const r = await fetch(`/api/okas/ara?q=${encodeURIComponent(q)}`);
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!d.success) throw new Error(d.error || 'OKAS araması başarısız');
|
||||
return d.kalemler || [];
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* "ORTOPEDİK VİDA" hiçbir ihale başlığında geçmiyor ama teklif cetvellerinde
|
||||
* geçiyor — o ihalelerin adı "VETERİNER İLAÇ ... SARF MALZEME".
|
||||
*/
|
||||
export async function urunAra(q, { skip = 0, take = 20, teknik = false } = {}) {
|
||||
const p = new URLSearchParams({ q, skip: String(skip), take: String(take) });
|
||||
export async function urunAra(q, { skip = 0, take = 20, teknik = false, okas = [] } = {}) {
|
||||
const p = new URLSearchParams({ q: q || '', skip: String(skip), take: String(take) });
|
||||
if (teknik) p.set('teknik', '1');
|
||||
if (okas.length) p.set('okas', okas.join(','));
|
||||
const r = await fetch(`/api/urun-ara?${p}`);
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!d.success) throw new Error(d.error || 'arama başarısız');
|
||||
|
||||
Reference in New Issue
Block a user