CPV/OKAS ham kod sınıflandırması: yayınlanmadan önce tamamlanan bir gate + sidebar filtresi

- server/cpv-classify.mjs (yeni): existing (kalem kodu) → keyword (66 kural, 45 OKAS_BOLUM kodunun tamamı kapsanıyor) → AI (analyzeSpec, sadece sözlükte gerçekten var olan kod kabul edilir) hibrit sınıflandırma
- server/index.js: cache.tenders'a eklenmeden ÖNCE her ihale için CPV sınıflandırması await'leniyor (mevcut "sektör" alanının aksine, sonradan zenginleştirilmiyor) — hem ilk yükleme hem arka plan tamamlama yolunda
- İKN bazlı kalıcı SQLite önbellek (history-store.mjs) — aynı ilan bir daha asla yeniden sınıflandırılmıyor
- /api/meta/filtreler artık cpvBolumler döndürüyor
- Frontend: Sektörler'in hemen altına CPV Kodu filtresi (arama + çoklu seçim), ihale kartında CPV rozeti, landing page'de yeni özellik: "AB uyumlu CPV sınıflandırması — Türkiye'de ilk"
This commit is contained in:
2026-08-30 14:00:56 +03:00
parent f98568da8e
commit c5662d2e60
10 changed files with 490 additions and 15 deletions
+13
View File
@@ -60,15 +60,23 @@ export const AppProvider = ({ children }) => {
const toggleSektor = (sek) =>
setSelectedSektorler((prev) => (prev.includes(sek) ? prev.filter((s) => s !== sek) : [...prev, sek]));
// CPV filtresi (bölüm kodu bazlı, çoklu seçim) — sektör deseniyle birebir aynı.
const [selectedCpvler, setSelectedCpvler] = useState([]);
const toggleCpv = (kod) =>
setSelectedCpvler((prev) => (prev.includes(kod) ? prev.filter((k) => k !== kod) : [...prev, kod]));
// Sektör/kurum listeleri sunucudan gelir; sabit liste yalnızca yedektir.
const [sektorListesi, setSektorListesi] = useState(SECTORS_LIST);
const [kurumTipiListesi, setKurumTipiListesi] = useState(INSTITUTION_TYPES_LIST);
// CPV bölüm listesi için statik bir yedek yok — yalnızca sunucudan gelir.
const [cpvBolumListesi, setCpvBolumListesi] = useState([]);
useEffect(() => {
fetch('/api/meta/filtreler')
.then((r) => r.json())
.then((d) => {
if (d?.sektorler?.length) setSektorListesi(d.sektorler);
if (d?.kurumTipleri?.length) setKurumTipiListesi(d.kurumTipleri);
if (d?.cpvBolumler?.length) setCpvBolumListesi(d.cpvBolumler);
})
.catch(() => {});
}, []);
@@ -468,6 +476,7 @@ export const AppProvider = ({ children }) => {
setSelectedAlimTuru('');
setSelectedKurumTipi('');
setSelectedSektorler([]);
setSelectedCpvler([]);
setQuickFilter('all');
};
@@ -505,6 +514,10 @@ export const AppProvider = ({ children }) => {
toggleSektor,
sektorListesi,
kurumTipiListesi,
selectedCpvler,
setSelectedCpvler,
toggleCpv,
cpvBolumListesi,
onlyValidOnly,
setOnlyValidOnly,
quickFilter,