import {matchesRegion,matchesProcurement} from '../services/savedFilterCriteria'; import { matchesTenderSearch, matchesDetailedSearch } from '../services/tenderSearch'; import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'; import { useApp } from '../context/AppContext'; import { TenderCard } from './TenderCard'; import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react'; import { createTenderWorkbook } from '../services/tenderWorkbook'; import { getToken } from '../services/auth'; import { useDeadlineClock } from '../hooks/useDeadlineClock'; import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER, compareTenderDeadlines, compareTenderPublications } from '../services/ihaleTarih'; // İl adları kaynaklar arasında farklı yazılabiliyor (Ankara / ANKARA); // filtre büyük-küçük harf ve Türkçe karakter duyarlılığına takılmasın. const normTr = (v) => String(v || '').trim().toLocaleUpperCase('tr-TR'); export const TenderList = () => { const { tenders, searchQuery, detailedQueries,setDetailedQueries, kalemEslesen, itemSearchState, retryItemSearch, uyumEsigi, selectedCity,selectedRegion,setSelectedRegion, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, onlyValidOnly, setOnlyValidOnly, favoriteKurumlar, oncekiGiris } = useApp(); const [sortBy, setSortBy] = useState('newest'); // 'newest', 'urgent', 'distant' // Sonsuz kaydırma: sayfa yerine aşağı indikçe liste büyür. // (Sayfalama 10 bin kayıtta 1000+ sayfa düğmesi basıyordu.) const SAYFA_ADIM = 20; const [gosterilen, setGosterilen] = useState(SAYFA_ADIM); const dipRef = useRef(null); const now = useDeadlineClock(); // Filter logic const filteredTenders = useMemo(() => { return tenders.filter(tender => { // Search Query if (!matchesTenderSearch(tender,searchQuery,kalemEslesen)) return false; if (!matchesDetailedSearch(tender,detailedQueries)) return false; // Geçmiş ihaleler: varsayılan olarak GİZLİ. Kullanıcı "geçmişi de göster" // derse (onlyValidOnly=false) hepsi gelir. Tarihi okunamayan kayıt gizlenmez. // Arşiv / sonuçlananlar gibi geçmişi hedefleyen hızlı filtrelerde kapı kapalı. if (onlyValidOnly && !GECMIS_FILTRELER.has(quickFilter) && !acikIhale(tender, now)) return false; // Hızlı filtre (üst kartlar + sol menü) — ölçütler ihaleTarih.js'de tek yerde if (!hizliFiltreGecer(tender, quickFilter, { favoriteKurumlar: favoriteKurumlar || [], oncekiGirisMs: oncekiGiris, uyumEsigi })) return false; // Dropdown Filters if (selectedCity && normTr(tender.sehir) !== normTr(selectedCity)) return false; if (!matchesProcurement(tender,selectedAlimTuru)) return false; if (!matchesRegion(tender,selectedRegion)) 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; }).sort((a, b) => { if (sortBy === 'uyum') return (b.aiMatchScore ?? -1) - (a.aiMatchScore ?? -1) || compareTenderPublications(a,b); if (sortBy === 'newest') return compareTenderPublications(a,b); if (sortBy === 'urgent') return compareTenderDeadlines(a,b); if (sortBy === 'distant') return compareTenderDeadlines(a,b,-1); return 0; }); }, [tenders, searchQuery, detailedQueries, kalemEslesen, uyumEsigi, selectedCity, selectedRegion, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly, now, favoriteKurumlar, oncekiGiris]); const gorunenTenders = filteredTenders.slice(0, gosterilen); const dahaVar = gosterilen < filteredTenders.length; // Baştan başlama YALNIZCA filtre/sıralama değişiminde olmalı. filteredTenders // dizisine bağlansaydı havuz her yenilendiğinde (10 dk) kullanıcı listenin // başına fırlardı. useEffect(() => { setGosterilen(SAYFA_ADIM); }, [ searchQuery, detailedQueries, selectedCity, selectedRegion, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly, ]); const dahaGoster = useCallback(() => { setGosterilen((n) => Math.min(n + SAYFA_ADIM, filteredTenders.length)); }, [filteredTenders.length]); // Dipteki işaretçi görünür olunca yükle. IntersectionObserver yoksa // (eski tarayıcı) aşağıdaki "Daha fazla göster" düğmesi devreye girer. useEffect(() => { const el = dipRef.current; if (!el || !dahaVar || typeof IntersectionObserver === 'undefined') return; const gozlemci = new IntersectionObserver( (girisler) => { if (girisler[0]?.isIntersecting) dahaGoster(); }, { rootMargin: '600px 0px' }, // ekrana girmeden önce yükle ); gozlemci.observe(el); return () => gozlemci.disconnect(); }, [dahaVar, dahaGoster]); const [exporting,setExporting] = useState(false); const [exportError,setExportError] = useState(null); const [preparedExport,setPreparedExport] = useState(null); const preparedUrl = useRef(null); const exportPending = useRef(false); const mounted = useRef(true); useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;if(preparedUrl.current)URL.revokeObjectURL(preparedUrl.current);};},[]); const handleExportExcel = async () => { if (exportPending.current) return; exportPending.current = true; setExporting(true); setExportError(null); if(preparedUrl.current) URL.revokeObjectURL(preparedUrl.current); preparedUrl.current=null;setPreparedExport(null); const session = getToken(); // Export the full filtered result at click time, including rows below the fold. const selected = filteredTenders.map(t=>({...t})); try { const bytes = await createTenderWorkbook(selected); if (!mounted.current || getToken() !== session) return; const url = URL.createObjectURL(new Blob([bytes], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})); preparedUrl.current=url; setPreparedExport({url,name:`ihaleist_ihale_listesi_${Date.now()}.xlsx`,count:selected.length, query:searchQuery.trim(),session,createdAt:new Date().toLocaleTimeString('tr-TR')}); } catch (error) { if (mounted.current && getToken() === session) setExportError(error.message); } finally { exportPending.current=false; if (mounted.current) setExporting(false); } }; return (
Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.
} {itemSearchState?.status === 'loading' &&Doküman kalemlerinde arama sürüyor; listedeki sonuçlar henüz genişleyebilir.
} {itemSearchState?.status === 'error' &&{itemSearchState.error} Başlık, kurum ve yüklü kart bilgileriyle arama sürüyor; sonuçlar eksik olabilir.
{preparedExport.createdAt} itibarıyla {preparedExport.count} ihale için Excel hazır{preparedExport.query ? ` — arama: ${preparedExport.query}` : ''}.
Excel dosyasını indir — {preparedExport.name}{exportError}
} {/* Tender Cards List */} {gorunenTenders.length === 0 ? (Lütfen arama kelimenizi değiştirin veya filtreleri temizleyin.
Tüm kayıtlar gösterildi — {filteredTenders.length.toLocaleString('tr-TR')} ihale
)}