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
+6 -3
View File
@@ -15,6 +15,7 @@ export const TenderList = () => {
selectedCity,
selectedAlimTuru,
selectedSektorler,
selectedCpvler,
selectedKurumTipi,
quickFilter,
onlyValidOnly,
@@ -53,6 +54,7 @@ export const TenderList = () => {
if (selectedCity && normTr(tender.sehir) !== normTr(selectedCity)) return false;
if (selectedAlimTuru && tender.alimTuru !== selectedAlimTuru) return false;
if (selectedSektorler.length && !selectedSektorler.includes(tender.sektor)) return false;
if (selectedCpvler.length && !(tender.cpvBolum || []).some(b => selectedCpvler.includes(b))) return false;
if (selectedKurumTipi && tender.kurumTipi !== selectedKurumTipi) return false;
return true;
@@ -62,7 +64,7 @@ export const TenderList = () => {
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
return 0;
});
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedKurumTipi, quickFilter, sortBy]);
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy]);
const totalPages = Math.max(1, Math.ceil(filteredTenders.length / itemsPerPage));
const paginatedTenders = filteredTenders.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
@@ -70,7 +72,7 @@ export const TenderList = () => {
const handleExportExcel = () => {
confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } });
const headers = ['IKN', 'Baslik', 'Kurum', 'Sehir', 'Tarih', 'Alim Turu', 'Sektor'];
const headers = ['IKN', 'Baslik', 'Kurum', 'Sehir', 'Tarih', 'Alim Turu', 'Sektor', 'CPV'];
const rows = filteredTenders.map(t => [
t.ikn,
`"${t.baslik.replace(/"/g, '""')}"`,
@@ -78,7 +80,8 @@ export const TenderList = () => {
t.sehir,
t.tarih,
t.alimTuru,
t.sektor
t.sektor,
`"${(t.cpvLabel || (t.cpv || []).join(', ') || '').replace(/"/g, '""')}"`
]);
const csvContent = 'data:text/csv;charset=utf-8,\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');