feat: ilan ve ihale tarihi etiketli gosteriliyor + 'sadece acik ihaleler' filtresi gercekten calisiyor (varsayilan acik)
This commit is contained in:
@@ -110,6 +110,7 @@ function mapDtRow(row) {
|
|||||||
brans: (det.bransKodlari || []).join(', ') || row.turu || 'Genel',
|
brans: (det.bransKodlari || []).join(', ') || row.turu || 'Genel',
|
||||||
kurumTipi: classifyKurum(kurum),
|
kurumTipi: classifyKurum(kurum),
|
||||||
eklenmeZamani: row.yayim_tarihi ? `Yayım: ${row.yayim_tarihi}` : '',
|
eklenmeZamani: row.yayim_tarihi ? `Yayım: ${row.yayim_tarihi}` : '',
|
||||||
|
ilanTarihi: row.yayim_tarihi || '',
|
||||||
isYeni: false,
|
isYeni: false,
|
||||||
isLiveFeed: true,
|
isLiveFeed: true,
|
||||||
aciliyetBadge: null,
|
aciliyetBadge: null,
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ function rowToTender(r) {
|
|||||||
items,
|
items,
|
||||||
// Arayüzün beklediği alanlar (geçmiş kayıtta karşılığı yok — güvenli varsayılan)
|
// Arayüzün beklediği alanlar (geçmiş kayıtta karşılığı yok — güvenli varsayılan)
|
||||||
eklenmeZamani: 'arşivden',
|
eklenmeZamani: 'arşivden',
|
||||||
|
ilanTarihi: r.ilan_tarihi || '',
|
||||||
isYeni: false,
|
isYeni: false,
|
||||||
aciliyetBadge: null,
|
aciliyetBadge: null,
|
||||||
usul: r.ihale_tipi || 'İhale',
|
usul: r.ihale_tipi || 'İhale',
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ function mapAd(ad) {
|
|||||||
brans: ihaleTuru || 'Genel',
|
brans: ihaleTuru || 'Genel',
|
||||||
kurumTipi: classify(INSTITUTION_RULES, ad.advertiserName || '', 'Diğer Kamu Kurumları'),
|
kurumTipi: classify(INSTITUTION_RULES, ad.advertiserName || '', 'Diğer Kamu Kurumları'),
|
||||||
eklenmeZamani: relativeTimeTr(ad.publishStartDate),
|
eklenmeZamani: relativeTimeTr(ad.publishStartDate),
|
||||||
|
ilanTarihi: ad.publishStartDate ? String(ad.publishStartDate).slice(0, 10) : '',
|
||||||
isYeni: Boolean(publishedToday),
|
isYeni: Boolean(publishedToday),
|
||||||
isLiveFeed: true,
|
isLiveFeed: true,
|
||||||
aciliyetBadge,
|
aciliyetBadge,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles } from 'lucide-react';
|
import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles, CalendarDays, Megaphone } from 'lucide-react';
|
||||||
|
|
||||||
export const TenderCard = ({ tender }) => {
|
export const TenderCard = ({ tender }) => {
|
||||||
const { toggleTrackTender, setActiveTenderDetail, favoriteKurumlar, toggleFavoriteKurum } = useApp();
|
const { toggleTrackTender, setActiveTenderDetail, favoriteKurumlar, toggleFavoriteKurum } = useApp();
|
||||||
@@ -9,6 +9,18 @@ export const TenderCard = ({ tender }) => {
|
|||||||
const [day, monthYear] = tender.tarih ? tender.tarih.split('.') : ['01', '01.2026'];
|
const [day, monthYear] = tender.tarih ? tender.tarih.split('.') : ['01', '01.2026'];
|
||||||
const monthYearFormatted = monthYear ? `${monthYear}` : '08.2026';
|
const monthYearFormatted = monthYear ? `${monthYear}` : '08.2026';
|
||||||
|
|
||||||
|
// İlan (yayım) tarihi kaynaklara göre farklı biçimde geliyor:
|
||||||
|
// canlı akış ISO (2026-08-30), DT/arşiv gg.aa.yyyy. İkisini de gösterebilelim.
|
||||||
|
const ilanTarihiGoster = (() => {
|
||||||
|
const v = tender.ilanTarihi;
|
||||||
|
if (!v) return null;
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(v)) {
|
||||||
|
const [y, a, g] = v.slice(0, 10).split('-');
|
||||||
|
return `${g}.${a}.${y}`;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl border border-blue-200/80 shadow-xs hover:shadow-md transition-all overflow-hidden flex flex-col md:flex-row group border-l-4 border-l-blue-600">
|
<div className="bg-white rounded-xl border border-blue-200/80 shadow-xs hover:shadow-md transition-all overflow-hidden flex flex-col md:flex-row group border-l-4 border-l-blue-600">
|
||||||
|
|
||||||
@@ -17,7 +29,8 @@ export const TenderCard = ({ tender }) => {
|
|||||||
onClick={() => setActiveTenderDetail(tender)}
|
onClick={() => setActiveTenderDetail(tender)}
|
||||||
className="bg-[#edf4ff] group-hover:bg-[#e2edff] text-blue-900 md:w-36 p-4 flex flex-col items-center justify-center shrink-0 border-b md:border-b-0 md:border-r border-blue-100 cursor-pointer select-none transition-colors"
|
className="bg-[#edf4ff] group-hover:bg-[#e2edff] text-blue-900 md:w-36 p-4 flex flex-col items-center justify-center shrink-0 border-b md:border-b-0 md:border-r border-blue-100 cursor-pointer select-none transition-colors"
|
||||||
>
|
>
|
||||||
<span className="text-3xl font-black tracking-tight text-blue-700">{day}</span>
|
<span className="text-[9px] font-black uppercase tracking-widest text-blue-900/50">İhale</span>
|
||||||
|
<span className="text-3xl font-black tracking-tight text-blue-700 leading-none">{day}</span>
|
||||||
<span className="text-xs font-bold text-blue-900/80 mt-0.5">{monthYearFormatted}</span>
|
<span className="text-xs font-bold text-blue-900/80 mt-0.5">{monthYearFormatted}</span>
|
||||||
<span className="text-[11px] font-semibold bg-white/80 px-2 py-0.5 rounded-full text-blue-800 mt-2 border border-blue-200">
|
<span className="text-[11px] font-semibold bg-white/80 px-2 py-0.5 rounded-full text-blue-800 mt-2 border border-blue-200">
|
||||||
{tender.saat}
|
{tender.saat}
|
||||||
@@ -75,10 +88,30 @@ export const TenderCard = ({ tender }) => {
|
|||||||
<MapPin className="w-3.5 h-3.5 text-rose-500 shrink-0" />
|
<MapPin className="w-3.5 h-3.5 text-rose-500 shrink-0" />
|
||||||
<span>{tender.sehir}</span>
|
<span>{tender.sehir}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-emerald-50 text-emerald-700 border border-emerald-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1">
|
{ilanTarihiGoster && (
|
||||||
<Clock className="w-3 h-3 text-emerald-600" />
|
<span
|
||||||
<span>{tender.eklenmeZamani}</span>
|
className="bg-emerald-50 text-emerald-700 border border-emerald-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1"
|
||||||
</span>
|
title="İlanın yayımlandığı tarih"
|
||||||
|
>
|
||||||
|
<Megaphone className="w-3 h-3 text-emerald-600" />
|
||||||
|
<span>İlan: {ilanTarihiGoster}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{tender.tarih && (
|
||||||
|
<span
|
||||||
|
className="bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1"
|
||||||
|
title="İhalenin yapılacağı tarih"
|
||||||
|
>
|
||||||
|
<CalendarDays className="w-3 h-3 text-blue-600" />
|
||||||
|
<span>İhale: {tender.tarih}{tender.saat ? ` ${tender.saat}` : ''}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{tender.eklenmeZamani && !ilanTarihiGoster && (
|
||||||
|
<span className="bg-slate-100 text-slate-600 border border-slate-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3 text-slate-500" />
|
||||||
|
<span>{tender.eklenmeZamani}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-slate-400 font-mono text-[11px]">
|
<span className="text-slate-400 font-mono text-[11px]">
|
||||||
# {tender.ikn}
|
# {tender.ikn}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import confetti from 'canvas-confetti';
|
|||||||
// filtre büyük-küçük harf ve Türkçe karakter duyarlılığına takılmasın.
|
// 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');
|
const normTr = (v) => String(v || '').trim().toLocaleUpperCase('tr-TR');
|
||||||
|
|
||||||
|
// "gg.aa.yyyy" → Date. Ayrıştırılamayanı GEÇMİŞ SAYMAYIZ: tarihi belirsiz bir
|
||||||
|
// ihaleyi sessizce gizlemek, geçmiş bir ihaleyi göstermekten daha kötüdür.
|
||||||
|
const ihaleGunu = (v) => {
|
||||||
|
const m = /^(\d{2})\.(\d{2})\.(\d{4})/.exec(String(v || '').trim());
|
||||||
|
if (!m) return null;
|
||||||
|
return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||||
|
};
|
||||||
|
|
||||||
export const TenderList = () => {
|
export const TenderList = () => {
|
||||||
const {
|
const {
|
||||||
tenders,
|
tenders,
|
||||||
@@ -19,6 +27,7 @@ export const TenderList = () => {
|
|||||||
selectedKurumTipi,
|
selectedKurumTipi,
|
||||||
quickFilter,
|
quickFilter,
|
||||||
onlyValidOnly,
|
onlyValidOnly,
|
||||||
|
setOnlyValidOnly,
|
||||||
favoriteKurumlar
|
favoriteKurumlar
|
||||||
} = useApp();
|
} = useApp();
|
||||||
|
|
||||||
@@ -30,6 +39,9 @@ export const TenderList = () => {
|
|||||||
const [gosterilen, setGosterilen] = useState(SAYFA_ADIM);
|
const [gosterilen, setGosterilen] = useState(SAYFA_ADIM);
|
||||||
const dipRef = useRef(null);
|
const dipRef = useRef(null);
|
||||||
|
|
||||||
|
// Bugünün başlangıcı — "bugün ihalesi olan" listede kalsın.
|
||||||
|
const bugun = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }, []);
|
||||||
|
|
||||||
// Filter logic
|
// Filter logic
|
||||||
const filteredTenders = useMemo(() => {
|
const filteredTenders = useMemo(() => {
|
||||||
return tenders.filter(tender => {
|
return tenders.filter(tender => {
|
||||||
@@ -43,6 +55,13 @@ export const TenderList = () => {
|
|||||||
if (!matchTitle && !matchKurum && !matchIkn && !matchItems) return false;
|
if (!matchTitle && !matchKurum && !matchIkn && !matchItems) 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.
|
||||||
|
if (onlyValidOnly) {
|
||||||
|
const gun = ihaleGunu(tender.tarih);
|
||||||
|
if (gun && gun < bugun) return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Quick Filter
|
// Quick Filter
|
||||||
if (quickFilter === 'favori_kurumlar' && !(favoriteKurumlar || []).includes((tender.kurum || '').trim())) return false;
|
if (quickFilter === 'favori_kurumlar' && !(favoriteKurumlar || []).includes((tender.kurum || '').trim())) return false;
|
||||||
if (quickFilter === 'dogrudan_temin' && tender.alimTuru !== 'Doğrudan Temin') return false;
|
if (quickFilter === 'dogrudan_temin' && tender.alimTuru !== 'Doğrudan Temin') return false;
|
||||||
@@ -68,7 +87,7 @@ export const TenderList = () => {
|
|||||||
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
|
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy]);
|
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly, bugun]);
|
||||||
|
|
||||||
const gorunenTenders = filteredTenders.slice(0, gosterilen);
|
const gorunenTenders = filteredTenders.slice(0, gosterilen);
|
||||||
const dahaVar = gosterilen < filteredTenders.length;
|
const dahaVar = gosterilen < filteredTenders.length;
|
||||||
@@ -78,7 +97,7 @@ export const TenderList = () => {
|
|||||||
// başına fırlardı.
|
// başına fırlardı.
|
||||||
useEffect(() => { setGosterilen(SAYFA_ADIM); }, [
|
useEffect(() => { setGosterilen(SAYFA_ADIM); }, [
|
||||||
searchQuery, selectedCity, selectedAlimTuru, selectedSektorler,
|
searchQuery, selectedCity, selectedAlimTuru, selectedSektorler,
|
||||||
selectedCpvler, selectedKurumTipi, quickFilter, sortBy,
|
selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dahaGoster = useCallback(() => {
|
const dahaGoster = useCallback(() => {
|
||||||
@@ -136,6 +155,20 @@ export const TenderList = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||||
|
{/* Varsayılan: yalnızca ihale tarihi geçmemiş ilanlar */}
|
||||||
|
<label
|
||||||
|
className="flex items-center gap-1.5 text-xs font-semibold text-slate-700 cursor-pointer select-none whitespace-nowrap"
|
||||||
|
title="Kapalı: ihale tarihi geçmiş ilanlar da listelenir"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={onlyValidOnly}
|
||||||
|
onChange={(e) => setOnlyValidOnly(e.target.checked)}
|
||||||
|
className="accent-blue-600"
|
||||||
|
/>
|
||||||
|
Sadece açık ihaleler
|
||||||
|
</label>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
|
|||||||
Reference in New Issue
Block a user