diff --git a/server/history-store.mjs b/server/history-store.mjs index 06ee580..6d288cd 100644 --- a/server/history-store.mjs +++ b/server/history-store.mjs @@ -6,7 +6,7 @@ */ import fs from 'node:fs'; -import { sektorBul } from './siniflandir.mjs'; +import { sektorKarar } from './siniflandir.mjs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { titleCaseTr } from './tr-text.mjs'; @@ -164,9 +164,9 @@ function rowToTender(r) { sehir: titleCaseTr(r.il), tarih: r.ihale_tarihi || '', alimTuru: r.ihale_tipi || 'İhale', - // Sektör ihale TÜRÜ değildir ('Mal Alımı' bir sektör değil); - // ihale adından ve kalem adlarından çıkarılır. - sektor: sektorBul(r.ihale_adi || '', kalemAdlari(r.okas_kalemler)), + // Sektör ihale TÜRÜ değildir ('Mal Alımı' bir sektör değil). + // Önce OKAS kodu (idarenin kendi sınıflandırması), yoksa ihale adı. + sektor: sektorKarar(okasParse(r.okas_kalemler, null), r.ihale_adi || ''), kazananFirma: r.kazanan_firma, sozlesmeBedeli: r.sozlesme_bedeli, paraBirimi: r.para_birimi, diff --git a/server/siniflandir.mjs b/server/siniflandir.mjs index 73bc9cf..91cfac0 100644 --- a/server/siniflandir.mjs +++ b/server/siniflandir.mjs @@ -33,47 +33,156 @@ export function normalizeTr(s) { // 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 = [ - [/tibbi|medikal|saglik|hastane|ilac |poliklinik|cerrahi|ameliyat|diyaliz|radyoloji|rontgen|tomografi|ultrason|steril|enjektor|eldiven|maske|pansuman|ped |sonda|kateter|protez|ortez|isitme cihaz|dis unit|endo motor|reaktif|hemogram|serum|asi |sedye|ligasyon|test kiti| kit alimi|manometre|oksijen regulator|medikal gaz|isitme engelli|induksiyon dongu|hasta yatak|tekerlekli sandalye|otoklav|aspirator|nebulizator|puls oksimetre/i, 'Sağlık / Tıbbi Malzeme'], - [/yazilim|bilisim|sunucu|donanim|network|bilgisayar|lisans|veri merkezi|siber|yazici alim|toner alim|tablet|projeksiyon|kart okuyucu|nitelikli sertifika|e-imza sertifika|barkod|e-imza|otomasyon sistem/i, 'Bilişim & Yazılım'], - [/guvenlik hizmet|ozel guvenlik|kamera|cctv|alarm|turnike|x-ray|guvenlik sistem|yangin sondur|yangin tup|yangin algila|itfaiye/i, 'Güvenlik Sistemleri'], - [/gida|yemek|catering|ekmek|et alim|kirmizi et|beyaz et|sebze|meyve|kumanya|sut |peynir|tavuk|bakliyat|mutfak|gazoz|icecek|su alimi|cips|kuru gida|dana |kuzu |fritoz|reso |benmari|bulasik makinesi|mutfak ekipman|kantin/i, 'Gıda & Catering'], - [/temizlik|hijyen|dezenfek|deterjan|cop |atik toplama|hasere|ilaclama|supurge|kagit havlu|tuvalet kagidi|konteyner|atik depo|geri donusum/i, 'Temizlik & Hijyen'], - [/asfalt|beton|yol yapim|kilit parke|bordur|kaldirim|kopru|altyapi|kanalizasyon|icme suyu|sulama|menfez|istinat|stabilize|dolgu/i, 'Altyapı & Yol'], - [/insaat|yapim isi|onarim|tamirat|tadilat|guclendirme|cati|cephe|restorasyon|bakim onarim|boya |badana|dograma|pvc pencere|hafriyat|yikim|nalbur|hirdavat|civata|vida |cimento|demir alim|kereste|fayans|seramik|izolasyon|mantolama/i, 'İnşaat & Bakım'], - [/kirtasiye|matbaa|baski|kagit|toner|kartus|fotokopi|buro malzeme|defter|album|dosya alim|zarf |basili evrak|form basim|kaset|klasor/i, 'Kırtasiye & Büro'], - [/mobilya|masa |sandalye|dolap|okul sirasi|buro mobilya|raf |perde|koltuk|yatak |karyola/i, 'Mobilya & Donatı'], - [/giyim|tekstil|elbise|uniforma|kiyafet|ayakkabi|bot alim|camasir|kumas|battaniye|havlu |nevresim/i, 'Tekstil & Giyim'], - [/akaryakit|motorin|benzin|lpg|yakit alim|odun |komur|pelet /i, 'Akaryakıt & Yakıt'], - [/arac kirala|tasit kirala|servis hizmet|personel tasima|ogrenci tasima|nakliye|lojistik|treyler|cekici kirala/i, 'Taşımacılık & Kiralama'], - [/arac |otomotiv|lastik|yedek parca|otobus|kamyon|is makinesi|greyder|ekskavator|kepce|traktor|transmisyon|motor yagi|aku |plakali|tasit bakim|arac bakim|balata|amortisor|rot |far alim/i, 'Otomotiv & İş Makinesi'], - [/elektrik|trafo|enerji|aydinlatma|jenerator|gunes|solar|kablo|pano |ups |armatur|ampul|led /i, 'Elektrik & Enerji'], - [/dogalgaz|kalorifer|kazan |isitma|sogutma|klima|havalandirma|iklimlendirme|mekanik tesisat|radyator|buzdolabi|sogutucu/i, 'Isıtma & İklimlendirme'], - [/asansor|yuruyen merdiven|platform lift/i, 'Asansör & Yürüyen Sistem'], - [/su aritma|aritma tesis|pompa|sondaj|kuyu |su deposu|klorlama|dalgic|boru |vana /i, 'Su & Arıtma'], - [/tarim|tohum|gubre|fide|zirai|hayvan|yem |veteriner|sera |ilaclama zirai|orman|agaclandirma/i, 'Tarım & Hayvancılık'], - [/peyzaj|park bahce|agac |cim |fidan|oyun grubu|kent mobilya|bank alim|cicek/i, 'Peyzaj & Park'], - [/egitim hizmet|kurs |seminer|danismanlik|musavirlik|etut proje|muhendislik hizmet|kontrolluk|arastirma hizmet|saha arastirma|anket/i, 'Danışmanlık & Proje'], - [/reklam|tanitim|organizasyon|fuar |basin|medya|produksiyon|afis|brosur|davetiye|promosyon|porselen kupa|hediyelik|bayrak alim|flama/i, 'Reklam & Organizasyon'], - [/sigorta|zorunlu trafik|kasko/i, 'Sigorta'], - [/kitap|yayin|kutuphane|arsiv|dijitallestirme|tercume|ceviri/i, 'Yayın & Arşiv'], - [/spor |hali saha|tribun|fitness|kondisyon|stadyum|yuzme havuz/i, 'Spor Tesisleri'], - [/laboratuvar|analiz cihaz|olcum cihaz|test cihaz|mikroskop|santrifuj|numune|kantar|tarti |terazi|kalibrasyon|sayac /i, 'Laboratuvar & Ölçüm'], - [/tasinmaz|parsel|arsa |gayrimenkul|kiraya veril|satis ihalesi|irtifak|ecrimisil/i, 'Taşınmaz & Kiralama'], - [/polisaj|testere|torna|kaynak makinesi|matkap|jeneratorlu|atolye|el aleti|kompresor/i, 'Atölye & Makine Ekipmanı'], - [/personel calistir|isci calistir|hizmet alimi personel|temizlik personel|guvenlik personel|destek personel/i, 'Personel Hizmet Alımı'], + [/\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|\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|\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 = [ - [/buyuksehir belediye/i, 'Büyükşehir Belediyeleri'], - [/belediye/i, 'Belediyeler'], - [/universite/i, 'Üniversiteler'], - [/hastane|saglik mudurlugu|bashekim/i, 'Eğitim ve Araştırma Hastaneleri'], - [/agiz ve dis/i, 'Ağız ve Diş Sağlığı Merkezleri'], - [/ceza infaz|cezaevi|tevkifevi/i, 'Ceza İnfaz Kurumları'], - [/bakanligi|bakanlik|valiligi|mudurlugu/i, 'Bakanlıklar'], - [/a\.s\.|genel mudurlugu|isletme/i, 'Kamu Şirketleri'], + [/\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'], ]; +// --- 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); @@ -86,6 +195,18 @@ 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ı'); }