255 lines
14 KiB
JavaScript
255 lines
14 KiB
JavaScript
/**
|
||
* Sektör ve kurum tipi sınıflandırması — TEK kaynak.
|
||
*
|
||
* Üç ayrı veri kaynağı (ilan.gov.tr canlı akışı, doğrudan temin, EKAP sonuç
|
||
* arşivi) aynı kurallardan geçer. Aksi halde "Sağlık" filtresi yalnızca bir
|
||
* kaynağı süzer, diğer iki kaynaktaki ihaleler görünmez olur.
|
||
*
|
||
* TÜRKÇE BÜYÜK HARF TUZAĞI: ilan başlıklarının yarısı TAMAMEN BÜYÜK HARF ve
|
||
* JavaScript'in `i` bayrağı Türkçe İ/I çiftini katlamaz — /inşaat/i, "İNŞAAT"
|
||
* ile EŞLEŞMEZ. Bu yüzden metni önce normalleştiriyoruz (Türkçe küçük harf +
|
||
* ASCII katlama) ve kuralları da normalleştirilmiş alfabede yazıyoruz:
|
||
* desenlerde ı/i/ş/ç/ğ/ö/ü YOKTUR, hepsi ASCII karşılığıdır.
|
||
*/
|
||
|
||
const TR_KATLAMA = {
|
||
'İ': 'i', 'I': 'i', 'ı': 'i', 'i': 'i',
|
||
'Ş': 's', 'ş': 's', 'Ç': 'c', 'ç': 'c',
|
||
'Ğ': 'g', 'ğ': 'g', 'Ö': 'o', 'ö': 'o',
|
||
'Ü': 'u', 'ü': 'u', 'Â': 'a', 'â': 'a', 'Î': 'i', 'î': 'i', 'Û': 'u', 'û': 'u',
|
||
};
|
||
|
||
/** Türkçe duyarlı küçültme + ASCII katlama. "İNŞAAT" → "insaat" */
|
||
export function normalizeTr(s) {
|
||
if (!s) return '';
|
||
let out = '';
|
||
for (const ch of String(s)) {
|
||
out += TR_KATLAMA[ch] ?? ch.toLowerCase();
|
||
}
|
||
return out.replace(/\s+/g, ' ');
|
||
}
|
||
|
||
// Desenler NORMALLEŞTİRİLMİŞ metne uygulanır: hepsi küçük harf ve ASCII.
|
||
// Sıra ÖNEMLİ: ilk eşleşen kazanır, dar kalıplar geniş olanlardan önce gelir
|
||
// (ör. "tibbi gaz" → Sağlık, "dogalgaz" → Isıtma).
|
||
export const SECTOR_RULES = [
|
||
[/\btibbi|\bmedikal|\bsaglik|\bhastane|\bilac\b|\bpoliklinik|\bcerrahi|\bameliyat|\bdiyaliz|\bradyoloji|\brontgen|\btomografi|\bultrason|\bsteril|\benjektor|\beldiven|\bmaske|\bpansuman|\bped\b|\bsonda|\bkateter|\bprotez|\bortez|\bisitme cihaz|\bdis unit|\bdental|\bdis hekim|\bimplant|\bendo motor|\breaktif|\bhemogram|\bserum|\basi alimi|\basilama|\bsedye|\bligasyon|\btest kiti|\bkit alimi|\bmanometre|\boksijen regulator|\bmedikal gaz|\bisitme engelli|\binduksiyon dongu|\bhasta yatak|\btekerlekli sandalye|\botoklav|\baspirator|\bnebulizator|\bpuls oksimetre/i, 'Sağlık / Tıbbi Malzeme'],
|
||
[/\byazilim|\bbilisim|\bsunucu|\bdonanim|\bnetwork|\bbilgisayar|\blisans|\bveri merkezi|\bsiber|\byazici alim|\btoner alim|\btablet|\bprojeksiyon|\bkart okuyucu|\bnitelikli sertifika|\be-imza sertifika|\bbarkod|\be-imza|\botomasyon sistem/i, 'Bilişim & Yazılım'],
|
||
[/\bguvenlik hizmet|\bozel guvenlik|\bkamera|\bcctv|\balarm|\bturnike|\bx-ray|\bguvenlik sistem|\byangin sondur|\byangin tup|\byangin algila|\bitfaiye/i, 'Güvenlik Sistemleri'],
|
||
[/\bgida|\byemek|\bcatering|\bekmek|\bet alim|\bkirmizi et|\bbeyaz et|\bsebze|\bmeyve|\bkumanya|\bsut\b|\bpeynir|\btavuk|\bbakliyat|\bmutfak|\bgazoz|\bicecek|\bsu alimi|\bcips|\bkuru gida|\bdana\b|\bkuzu\b|\bfritoz|\breso\b|\bbenmari|\bbulasik makinesi|\bmutfak ekipman|\bkantin/i, 'Gıda & Catering'],
|
||
[/\btemizlik|\bhijyen|\bdezenfek|\bdeterjan|\bcop\b|\batik toplama|\bhasere|\bilaclama|\bsupurge|\bkagit havlu|\btuvalet kagidi|\bkonteyner|\batik depo|\bgeri donusum/i, 'Temizlik & Hijyen'],
|
||
[/\basfalt|\bbeton|\byol yapim|\bkilit parke|\bbordur|\bkaldirim|\bkopru|\baltyapi|\bkanalizasyon|\bicme suyu|\bsulama|\bmenfez|\bistinat|\bstabilize|\bdolgu/i, 'Altyapı & Yol'],
|
||
[/\binsaat|\byapim isi|\bonarim|\btamirat|\btadilat|\bguclendirme|\bcati|\bcephe|\brestorasyon|\bbakim onarim|\bbakim ve onarim|\bbakim hizmet|\bbakim ve onarim|\bbakim isi\b|\bbakim hizmet|\bboya\b|\bbadana|\bdograma|\bpvc pencere|\bhafriyat|\byikim|\bnalbur|\bhirdavat|\bcivata|\bvida\b|\bcimento|\bdemir alim|\bkereste|\bfayans|\bseramik|\bizolasyon|\bmantolama/i, 'İnşaat & Bakım'],
|
||
[/\bkirtasiye|\bmatbaa|\bbaski|\bkagit|\btoner|\bkartus|\bfotokopi|\bburo malzeme|\bdefter|\balbum|\bdosya alim|\bzarf\b|\bbasili evrak|\bform basim|\bkaset|\bklasor/i, 'Kırtasiye & Büro'],
|
||
[/\bmobilya|\bmasa\b|\bsandalye|\bdolap|\bokul sirasi|\bburo mobilya|\braf\b|\bperde|\bkoltuk|\byatak\b|\bkaryola/i, 'Mobilya & Donatı'],
|
||
[/\bgiyim|\btekstil|\belbise|\buniforma|\bkiyafet|\bayakkabi|\bbot alim|\bcamasir|\bkumas|\bbattaniye|\bhavlu\b|\bnevresim/i, 'Tekstil & Giyim'],
|
||
[/\bakaryakit|\bmotorin|\bbenzin|\blpg|\byakit alim|\bodun\b|\bkomur|\bpelet\b/i, 'Akaryakıt & Yakıt'],
|
||
[/\barac kirala|\btasit kirala|\bservis hizmet|\bpersonel tasima|\bogrenci tasima|\btasimali egitim|\btasimali ilkogretim|\btasimali ortaogretim|\bogrenci.{0,30}tasin|\bogrenci.{0,30}tasima|\btasinmasi isi|\btasinmasi hizmet|\btasima hizmet|\bnakliye|\blojistik|\btreyler|\bcekici kirala/i, 'Taşımacılık & Kiralama'],
|
||
[/\barac\b|\botomotiv|\blastik|\byedek parca|\botobus|\bkamyon|\bis makinesi|\bgreyder|\bekskavator|\bkepce|\btraktor|\btransmisyon|\bmotor yagi|\baku\b|\bplakali|\btasit bakim|\barac bakim|\bbalata|\bamortisor|\brot\b|\bfar alim/i, 'Otomotiv & İş Makinesi'],
|
||
[/\belektrik|\btrafo|\benerji|\baydinlatma|\bjenerator|\bgunes|\bsolar|\bkablo|\bpano\b|\bups\b|\barmatur|\bampul|\bled\b/i, 'Elektrik & Enerji'],
|
||
[/\bdogalgaz|\bkalorifer|\bkazan\b|\bisitma|\bsogutma|\bklima|\bhavalandirma|\biklimlendirme|\bmekanik tesisat|\bradyator|\bbuzdolabi|\bsogutucu/i, 'Isıtma & İklimlendirme'],
|
||
[/\basansor|\byuruyen merdiven|\bplatform lift/i, 'Asansör & Yürüyen Sistem'],
|
||
[/\bsu aritma|\baritma tesis|\bpompa|\bsondaj|\bkuyu\b|\bsu deposu|\bklorlama|\bdalgic|\bboru\b|\bvana\b/i, 'Su & Arıtma'],
|
||
[/\btarim|\btohum|\bgubre|\bfide|\bzirai|\bhayvan|\byem\b|\bveteriner|\bmama\b|\bkedi\b|\bkopek\b|\bevcil|\bsera\b|\bilaclama zirai|\borman|\bagaclandirma/i, 'Tarım & Hayvancılık'],
|
||
[/\bpeyzaj|\bpark bahce|\bagac\b|\bcim\b|\bfidan|\boyun grubu|\bkent mobilya|\bbank alim|\bcicek/i, 'Peyzaj & Park'],
|
||
[/\begitim hizmet|\bkurs\b|\bseminer|\bdanismanlik|\bmusavirlik|\betut proje|\bmuhendislik hizmet|\bkontrolluk|\barastirma hizmet|\bsaha arastirma|\banket/i, 'Danışmanlık & Proje'],
|
||
[/\breklam|\btanitim|\borganizasyon|\bfuar\b|\bbasin|\bmedya|\bproduksiyon|\bafis|\bbrosur|\bdavetiye|\bpromosyon|\bporselen kupa|\bhediyelik|\bbayrak alim|\bflama/i, 'Reklam & Organizasyon'],
|
||
[/\bsigorta|\bzorunlu trafik|\bkasko/i, 'Sigorta'],
|
||
[/\bkitap|\byayin|\bkutuphane|\barsiv|\bdijitallestirme|\btercume|\bceviri/i, 'Yayın & Arşiv'],
|
||
[/\bspor\b|\bhali saha|\btribun|\bfitness|\bkondisyon|\bstadyum|\byuzme havuz/i, 'Spor Tesisleri'],
|
||
[/\blaboratuvar|\banaliz cihaz|\bolcum cihaz|\btest cihaz|\bmikroskop|\bsantrifuj|\bnumune|\bkantar|\btarti\b|\bterazi|\bkalibrasyon|\bsayac\b/i, 'Laboratuvar & Ölçüm'],
|
||
[/\btasinmaz|\bparsel|\barsa\b|\bgayrimenkul|\bkiraya veril|\bsatis ihalesi|\birtifak|\becrimisil/i, 'Taşınmaz & Kiralama'],
|
||
[/\bpolisaj|\btestere|\btorna|\bkaynak makinesi|\bmatkap|\bjeneratorlu|\batolye|\bel aleti|\bkompresor/i, 'Atölye & Makine Ekipmanı'],
|
||
[/\bpersonel calistir|\bisci calistir|\bhizmet alimi personel|\btemizlik personel|\bguvenlik personel|\bdestek personel/i, 'Personel Hizmet Alımı'],
|
||
];
|
||
|
||
export const INSTITUTION_RULES = [
|
||
[/\bbuyuksehir belediye/i, 'Büyükşehir Belediyeleri'],
|
||
[/\bbelediye/i, 'Belediyeler'],
|
||
[/\buniversite/i, 'Üniversiteler'],
|
||
[/\bhastane|\bsaglik mudurlugu|\bbashekim/i, 'Eğitim ve Araştırma Hastaneleri'],
|
||
[/\bagiz ve dis/i, 'Ağız ve Diş Sağlığı Merkezleri'],
|
||
[/\bceza infaz|\bcezaevi|\btevkifevi/i, 'Ceza İnfaz Kurumları'],
|
||
[/\bbakanligi|\bbakanlik|\bvaliligi|\bmudurlugu/i, 'Bakanlıklar'],
|
||
[/a\.s\.|\bgenel mudurlugu|\bisletme/i, 'Kamu Şirketleri'],
|
||
];
|
||
|
||
/**
|
||
* İlan metninden YALNIZCA işin konusunu ayıklar.
|
||
*
|
||
* İlanın tamamı standart hukuki metinle doludur ("e-imza ile imzalanarak",
|
||
* "numune sunulması istenmektedir", "…Sağlık Bakanlığı", "lisans belgesi") ve
|
||
* bunlar sınıflandırmayı zehirler: "Kedi ve köpek maması" ilanı e-imza
|
||
* cümlesi yüzünden Bilişim çıkar. Bu yüzden yalnızca iki güvenilir bölgeyi
|
||
* okuruz:
|
||
* 1) Başlangıç → "4734 sayılı" (ilanın konu cümlesi)
|
||
* 2) "Niteliği, türü ve miktarı" alanının değeri
|
||
*/
|
||
export function ilanKonusu(metin) {
|
||
if (!metin) return '';
|
||
// Arama da dönüş de NORMALLEŞTİRİLMİŞ metin üzerinde: "4734 sayılı" gibi
|
||
// işaretleri Türkçe karakterden bağımsız bulabilelim.
|
||
const t = normalizeTr(
|
||
String(metin).replace(/\{[^}]*\}/g, ' ').replace(/ /g, ' ')
|
||
);
|
||
|
||
const parcalar = [];
|
||
|
||
// 1) Konu cümlesi: metnin başından "4734 sayılı"ya kadar
|
||
const kanun = t.search(/4734\s*sayili|sayili kamu ihale kanunu/i);
|
||
parcalar.push(kanun > 0 ? t.slice(0, kanun) : t.slice(0, 300));
|
||
|
||
// 2) "Niteliği, türü ve miktarı" alanının değeri (bir sonraki madde numarasına kadar)
|
||
const nitelik = t.match(/nitelig[iı][^:]{0,40}:\s*([\s\S]{0,400}?)(?=\d+\.\d|\d+-\s|$)/i);
|
||
if (nitelik) parcalar.push(nitelik[1]);
|
||
|
||
return parcalar.join(' ').slice(0, 800);
|
||
}
|
||
|
||
// --- OKAS (CPV) kod sınıflandırması ---------------------------------------
|
||
// OKAS, AB'nin CPV numaralandırmasını kullanır ve kodu ihaleyi AÇAN İDARE atar.
|
||
// Yani başlıktan tahmin etmekten çok daha güvenilir bir sinyaldir; kod varsa
|
||
// metin kurallarından ÖNCE gelir.
|
||
//
|
||
// İlk iki hane "bölüm"dür. Bazı bölümler karışıktır (39 = hem mobilya hem
|
||
// temizlik ürünleri, 79 = hem güvenlik hem matbaa hem danışmanlık); onlar için
|
||
// daha uzun ön ek tanımlanır ve uzun olan kazanır.
|
||
|
||
const OKAS_GRUP = {
|
||
// 39 — mobilya / ev aletleri / temizlik ürünleri aynı bölümde
|
||
'3971': 'Isıtma & İklimlendirme', // elektrikli ev aletleri (buzdolabı, klima)
|
||
'3983': 'Temizlik & Hijyen', // temizlik ürünleri
|
||
'3984': 'Temizlik & Hijyen',
|
||
// 79 — iş hizmetleri: güvenlik, matbaa, reklam, danışmanlık, personel
|
||
'7971': 'Güvenlik Sistemleri',
|
||
'7972': 'Güvenlik Sistemleri',
|
||
'7981': 'Kırtasiye & Büro', // basım hizmetleri
|
||
'7982': 'Reklam & Organizasyon',
|
||
'7934': 'Reklam & Organizasyon',
|
||
'7995': 'Reklam & Organizasyon', // fuar/festival organizasyonu
|
||
'7962': 'Personel Hizmet Alımı',
|
||
'7963': 'Personel Hizmet Alımı',
|
||
// 34 — taşıt bölümünde yol donanımı da var
|
||
'3492': 'Altyapı & Yol', // yol donanımı, sinyalizasyon
|
||
'34928': 'Temizlik & Hijyen', // çöp konteyneri
|
||
// 37 — spor/oyun ekipmanı
|
||
'3753': 'Peyzaj & Park', // oyun grupları
|
||
// 44 — yapı malzemesi içinde boru/vana
|
||
'4416': 'Su & Arıtma',
|
||
// 90 — çevre hizmetleri içinde atık su
|
||
'9042': 'Su & Arıtma',
|
||
};
|
||
|
||
const OKAS_BOLUM = {
|
||
'03': 'Tarım & Hayvancılık',
|
||
'09': 'Akaryakıt & Yakıt',
|
||
'14': 'İnşaat & Bakım',
|
||
'15': 'Gıda & Catering',
|
||
'16': 'Tarım & Hayvancılık',
|
||
'18': 'Tekstil & Giyim',
|
||
'19': 'Tekstil & Giyim',
|
||
'22': 'Kırtasiye & Büro',
|
||
'24': 'İnşaat & Bakım',
|
||
'30': 'Bilişim & Yazılım',
|
||
'31': 'Elektrik & Enerji',
|
||
'32': 'Bilişim & Yazılım',
|
||
'33': 'Sağlık / Tıbbi Malzeme',
|
||
'34': 'Otomotiv & İş Makinesi',
|
||
'35': 'Güvenlik Sistemleri',
|
||
'37': 'Spor Tesisleri',
|
||
'38': 'Laboratuvar & Ölçüm',
|
||
'39': 'Mobilya & Donatı',
|
||
'41': 'Su & Arıtma',
|
||
'42': 'Atölye & Makine Ekipmanı',
|
||
'43': 'Otomotiv & İş Makinesi',
|
||
'44': 'İnşaat & Bakım',
|
||
'45': 'İnşaat & Bakım',
|
||
'48': 'Bilişim & Yazılım',
|
||
'50': 'İnşaat & Bakım',
|
||
'51': 'İnşaat & Bakım',
|
||
'55': 'Gıda & Catering',
|
||
'60': 'Taşımacılık & Kiralama',
|
||
'63': 'Taşımacılık & Kiralama',
|
||
'64': 'Bilişim & Yazılım',
|
||
'65': 'Elektrik & Enerji',
|
||
'66': 'Sigorta',
|
||
'70': 'Taşınmaz & Kiralama',
|
||
'71': 'Danışmanlık & Proje',
|
||
'72': 'Bilişim & Yazılım',
|
||
'73': 'Danışmanlık & Proje',
|
||
'75': 'Danışmanlık & Proje',
|
||
'76': 'Akaryakıt & Yakıt',
|
||
'77': 'Peyzaj & Park',
|
||
'79': 'Danışmanlık & Proje',
|
||
'80': 'Danışmanlık & Proje',
|
||
'85': 'Sağlık / Tıbbi Malzeme',
|
||
'90': 'Temizlik & Hijyen',
|
||
'92': 'Spor Tesisleri',
|
||
'98': 'Danışmanlık & Proje',
|
||
};
|
||
|
||
/** Tek bir OKAS kodundan sektör. En uzun ön ek kazanır. */
|
||
export function okasSektor(kod) {
|
||
const s = String(kod || '').replace(/\D/g, '');
|
||
if (s.length < 2) return null;
|
||
for (const n of [5, 4]) {
|
||
const hit = OKAS_GRUP[s.slice(0, n)];
|
||
if (hit) return hit;
|
||
}
|
||
return OKAS_BOLUM[s.slice(0, 2)] || null;
|
||
}
|
||
|
||
/**
|
||
* Kalem listesinden sektör — ÇOĞUNLUK oyu.
|
||
* Bir ihalede farklı bölümlerden kalemler olabilir (ör. hem inşaat hem
|
||
* elektrik); en çok tekrar eden bölüm ihalenin ağırlığını temsil eder.
|
||
*/
|
||
export function okasSektorListe(kalemler) {
|
||
if (!Array.isArray(kalemler) || !kalemler.length) return null;
|
||
const sayac = new Map();
|
||
for (const k of kalemler) {
|
||
const sek = okasSektor(k?.kod ?? k);
|
||
if (sek) sayac.set(sek, (sayac.get(sek) || 0) + 1);
|
||
}
|
||
if (!sayac.size) return null;
|
||
return [...sayac.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
||
}
|
||
|
||
/** Kuralları NORMALLEŞTİRİLMİŞ metne uygular. */
|
||
export function classify(rules, text, fallback) {
|
||
const t = normalizeTr(text);
|
||
for (const [re, label] of rules) if (re.test(t)) return label;
|
||
return fallback;
|
||
}
|
||
|
||
/** İhale metninden sektör. Kaynak ne olursa olsun aynı sonucu verir. */
|
||
export function sektorBul(...parcalar) {
|
||
return classify(SECTOR_RULES, parcalar.filter(Boolean).join(' '), 'Genel');
|
||
}
|
||
|
||
/**
|
||
* Sektör kararı: önce OKAS kodu (idarenin kendi sınıflandırması), yoksa metin.
|
||
* @param {Array} kalemler {kod, ad} listesi
|
||
* @param {...string} parcalar başlık vb. yedek metin
|
||
*/
|
||
export function sektorKarar(kalemler, ...parcalar) {
|
||
const koddan = okasSektorListe(kalemler);
|
||
if (koddan) return koddan;
|
||
const adlar = (kalemler || []).map((k) => k?.ad || '').join(' ');
|
||
return sektorBul(...parcalar, adlar);
|
||
}
|
||
|
||
export function kurumTipiBul(ad) {
|
||
return classify(INSTITUTION_RULES, ad || '', 'Diğer Kamu Kurumları');
|
||
}
|
||
|
||
/** Arayüzün kullandığı listeler — kurallardan türetilir, elle tutulmaz. */
|
||
// 'Genel' bir kural değil, hiçbir kalıba uymayanların düştüğü kova. Yine de
|
||
// listeye eklenir: aksi halde bu kayıtlar sektör filtresiyle hiç bulunamaz.
|
||
export const SEKTOR_ETIKETLERI = [
|
||
...[...new Set(SECTOR_RULES.map(([, ad]) => ad))].sort((a, b) => a.localeCompare(b, 'tr')),
|
||
'Genel',
|
||
];
|
||
export const KURUM_ETIKETLERI = [...new Set(INSTITUTION_RULES.map(([, ad]) => ad))]
|
||
.sort((a, b) => a.localeCompare(b, 'tr'));
|