fix: align list expiry and date ordering with tender deadlines
This commit is contained in:
@@ -214,3 +214,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
|
|||||||
- Service worker v3 yalnız bilinen kabuk ve varlık yollarını önbellekliyor. API, authorization başlıklı istekler ve rastgele GET yolları ağda kalıyor. Başarısız/yanlış MIME yanıtları sağlam kabuğu veya JS/CSS’yi bozamıyor; HTML yedeği varlık isteğine dönmüyor. Aktivasyon sadece eski İhaleist cache adlarını temizliyor.
|
- Service worker v3 yalnız bilinen kabuk ve varlık yollarını önbellekliyor. API, authorization başlıklı istekler ve rastgele GET yolları ağda kalıyor. Başarısız/yanlış MIME yanıtları sağlam kabuğu veya JS/CSS’yi bozamıyor; HTML yedeği varlık isteğine dönmüyor. Aktivasyon sadece eski İhaleist cache adlarını temizliyor.
|
||||||
- Kanıt: gerçek sw.js kodunu VM içinde fetch/activate olaylarıyla çalıştıran iki regresyon eklendi; 59/59 test ve üretim derlemesi geçti. API/özel yol, authorization, sunucu 500, offline kabuk, offline asset, yanlış MIME ve başka uygulama cache’ini koruma kontrol edildi.
|
- Kanıt: gerçek sw.js kodunu VM içinde fetch/activate olaylarıyla çalıştıran iki regresyon eklendi; 59/59 test ve üretim derlemesi geçti. API/özel yol, authorization, sunucu 500, offline kabuk, offline asset, yanlış MIME ve başka uygulama cache’ini koruma kontrol edildi.
|
||||||
- Sınır: VM testleri gerçek service worker kurulumu/güncellemesi veya cihazda offline kullanım kabulü değildir. Bunlar ve hesap değişiminin gerçek tarayıcıdaki tüm PWA senaryoları hâlâ açık.
|
- Sınır: VM testleri gerçek service worker kurulumu/güncellemesi veya cihazda offline kullanım kabulü değildir. Bunlar ve hesap değişiminin gerçek tarayıcıdaki tüm PWA senaryoları hâlâ açık.
|
||||||
|
|
||||||
|
### D02 / D05 — Ana liste tarih filtresi ve sıralaması
|
||||||
|
|
||||||
|
- Acil/uzak sıralamada gg.aa.yyyy metninin alfabetik karşılaştırılması kaldırıldı; yıl/ay/gün ve aynı gün içinde saat karşılaştırılıyor. Tarihi bilinmeyenler iki yönde de sonda kalıyor.
|
||||||
|
- Ana liste, özet kartları ve canlı üst sayacın açık ihale hesabı ortak deadlineInfo ile çalışıyor; 30 saniyelik saat güncellemesi sayfa açıkken süre dolmasını yansıtıyor. İptal/sonuç/sözleşme ve kazananı bilinen kayıtlar açık kabul edilmiyor. Tarih veya saat bilinmiyorsa uydurulmuyor.
|
||||||
|
- Filtre etiketi ve açıklaması eksik tarihli kaydın kesin açık olduğu izlenimini azaltacak şekilde güncellendi; sıralama alanına erişilebilir ad eklendi. Bugün/önceki ziyaret gün anahtarı Türkiye saatine bağlandı.
|
||||||
|
- Kanıt: 61/61 test ve build geçti. Ay/yıl geçişi, aynı gün saat sırası, bilinmeyen tarih, tam son teklif anı, eksik saatte Türkiye gece yarısı ve kapalı durumlar test edildi. Yeni eklenenler sıralamasının kayıt kimliği kullanımı, tüm kaynakların ilan zamanı doğrulaması ve tarayıcıda saat ilerletme kabulü hâlâ açık.
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { Database, PlusCircle, Eye, Layers, Trophy, TrendingDown } from 'lucide-react';
|
import { Database, PlusCircle, Eye, Layers, Trophy, TrendingDown } from 'lucide-react';
|
||||||
import { bugunEklendi, acikIhale } from '../services/ihaleTarih';
|
import { bugunEklendi, acikIhale } from '../services/ihaleTarih';
|
||||||
|
|
||||||
export const LiveStatsBar = () => {
|
export const LiveStatsBar = () => {
|
||||||
|
const now = useDeadlineClock();
|
||||||
const { liveStats, tenders } = useApp();
|
const { liveStats, tenders } = useApp();
|
||||||
|
|
||||||
const yukluIhale = tenders.length;
|
const yukluIhale = tenders.length;
|
||||||
@@ -11,9 +13,9 @@ export const LiveStatsBar = () => {
|
|||||||
const kalemSayisi = tenders.reduce((s, t) => s + (t.items?.length || 0), 0);
|
const kalemSayisi = tenders.reduce((s, t) => s + (t.items?.length || 0), 0);
|
||||||
// Üst kartla (BUGÜN EKLENENLER) AYNI ölçüt: bugün yayımlanan ve süresi
|
// Üst kartla (BUGÜN EKLENENLER) AYNI ölçüt: bugün yayımlanan ve süresi
|
||||||
// geçmemiş ilanlar (ilan.gov.tr + DT). İki bant farklı sayı göstermesin.
|
// geçmemiş ilanlar (ilan.gov.tr + DT). İki bant farklı sayı göstermesin.
|
||||||
const bugun = new Date(); bugun.setHours(0, 0, 0, 0);
|
|
||||||
const bugunEklenen = tenders.length
|
const bugunEklenen = tenders.length
|
||||||
? tenders.filter((t) => acikIhale(t, bugun) && bugunEklendi(t)).length
|
? tenders.filter((t) => acikIhale(t, now) && bugunEklendi(t)).length
|
||||||
: liveStats.bugunEklenen;
|
: liveStats.bugunEklenen;
|
||||||
|
|
||||||
// Havuz sağlığı: kaynak (ilan.gov.tr) sayısı 7 GÜNLÜK ORTALAMANIN çok altındaysa
|
// Havuz sağlığı: kaynak (ilan.gov.tr) sayısı 7 GÜNLÜK ORTALAMANIN çok altındaysa
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
|
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useApp } from '../context/AppContext';
|
import { useApp } from '../context/AppContext';
|
||||||
import { Layers, CalendarPlus, Star, BellRing, EyeOff } from 'lucide-react';
|
import { Layers, CalendarPlus, Star, BellRing, EyeOff } from 'lucide-react';
|
||||||
import { acikIhale, hizliFiltreGecer } from '../services/ihaleTarih';
|
import { acikIhale, hizliFiltreGecer } from '../services/ihaleTarih';
|
||||||
|
|
||||||
export const SummaryCards = () => {
|
export const SummaryCards = () => {
|
||||||
|
const now = useDeadlineClock();
|
||||||
const { tenders, quickFilter, setQuickFilter, setActiveTab, oncekiGiris, favoriteKurumlar } = useApp();
|
const { tenders, quickFilter, setQuickFilter, setActiveTab, oncekiGiris, favoriteKurumlar } = useApp();
|
||||||
|
|
||||||
// Sayımlar listedeki filtreyle AYNI ölçütü kullanır (ihaleTarih.js); karta
|
// Sayımlar listedeki filtreyle AYNI ölçütü kullanır (ihaleTarih.js); karta
|
||||||
// tıklayınca gelen liste ile karttaki sayı birbirini tutar.
|
// tıklayınca gelen liste ile karttaki sayı birbirini tutar.
|
||||||
const sayim = useMemo(() => {
|
const sayim = useMemo(() => {
|
||||||
const bugun = new Date(); bugun.setHours(0, 0, 0, 0);
|
|
||||||
const secenek = { favoriteKurumlar: favoriteKurumlar || [], oncekiGirisMs: oncekiGiris };
|
const secenek = { favoriteKurumlar: favoriteKurumlar || [], oncekiGirisMs: oncekiGiris };
|
||||||
const say = (id) => tenders.filter((t) => acikIhale(t, bugun) && hizliFiltreGecer(t, id, secenek)).length;
|
const say = (id) => tenders.filter((t) => acikIhale(t, now) && hizliFiltreGecer(t, id, secenek)).length;
|
||||||
return {
|
return {
|
||||||
all: say('all'),
|
all: say('all'),
|
||||||
bugun_eklenenler: say('bugun_eklenenler'),
|
bugun_eklenenler: say('bugun_eklenenler'),
|
||||||
@@ -19,7 +20,7 @@ export const SummaryCards = () => {
|
|||||||
son_giris: say('son_giris'),
|
son_giris: say('son_giris'),
|
||||||
incelenmedi: say('incelenmedi'),
|
incelenmedi: say('incelenmedi'),
|
||||||
};
|
};
|
||||||
}, [tenders, oncekiGiris, favoriteKurumlar]);
|
}, [tenders, oncekiGiris, favoriteKurumlar, now]);
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useApp } from '../context/AppContext';
|
|||||||
import { TenderCard } from './TenderCard';
|
import { TenderCard } from './TenderCard';
|
||||||
import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react';
|
import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react';
|
||||||
import confetti from 'canvas-confetti';
|
import confetti from 'canvas-confetti';
|
||||||
import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER } from '../services/ihaleTarih';
|
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||||
|
import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER, compareTenderDeadlines } from '../services/ihaleTarih';
|
||||||
|
|
||||||
// İl adları kaynaklar arasında farklı yazılabiliyor (Ankara / ANKARA);
|
// İ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.
|
// filtre büyük-küçük harf ve Türkçe karakter duyarlılığına takılmasın.
|
||||||
@@ -35,8 +36,7 @@ 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 now = useDeadlineClock();
|
||||||
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(() => {
|
||||||
@@ -57,7 +57,7 @@ export const TenderList = () => {
|
|||||||
// Geçmiş ihaleler: varsayılan olarak GİZLİ. Kullanıcı "geçmişi de göster"
|
// 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.
|
// derse (onlyValidOnly=false) hepsi gelir. Tarihi okunamayan kayıt gizlenmez.
|
||||||
// Arşiv / sonuçlananlar gibi geçmişi hedefleyen hızlı filtrelerde kapı kapalı.
|
// Arşiv / sonuçlananlar gibi geçmişi hedefleyen hızlı filtrelerde kapı kapalı.
|
||||||
if (onlyValidOnly && !GECMIS_FILTRELER.has(quickFilter) && !acikIhale(tender, bugun)) return false;
|
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
|
// 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;
|
if (!hizliFiltreGecer(tender, quickFilter, { favoriteKurumlar: favoriteKurumlar || [], oncekiGirisMs: oncekiGiris, uyumEsigi })) return false;
|
||||||
@@ -73,11 +73,11 @@ export const TenderList = () => {
|
|||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
if (sortBy === 'uyum') return (b.aiMatchScore ?? -1) - (a.aiMatchScore ?? -1) || b.id.localeCompare(a.id);
|
if (sortBy === 'uyum') return (b.aiMatchScore ?? -1) - (a.aiMatchScore ?? -1) || b.id.localeCompare(a.id);
|
||||||
if (sortBy === 'newest') return b.id.localeCompare(a.id);
|
if (sortBy === 'newest') return b.id.localeCompare(a.id);
|
||||||
if (sortBy === 'urgent') return a.tarih.localeCompare(b.tarih);
|
if (sortBy === 'urgent') return compareTenderDeadlines(a,b);
|
||||||
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
|
if (sortBy === 'distant') return compareTenderDeadlines(a,b,-1);
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}, [tenders, searchQuery, kalemEslesen, uyumEsigi, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly, bugun, favoriteKurumlar, oncekiGiris]);
|
}, [tenders, searchQuery, kalemEslesen, uyumEsigi, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy, onlyValidOnly, now, favoriteKurumlar, oncekiGiris]);
|
||||||
|
|
||||||
const gorunenTenders = filteredTenders.slice(0, gosterilen);
|
const gorunenTenders = filteredTenders.slice(0, gosterilen);
|
||||||
const dahaVar = gosterilen < filteredTenders.length;
|
const dahaVar = gosterilen < filteredTenders.length;
|
||||||
@@ -148,7 +148,7 @@ export const TenderList = () => {
|
|||||||
{/* Varsayılan: yalnızca ihale tarihi geçmemiş ilanlar */}
|
{/* Varsayılan: yalnızca ihale tarihi geçmemiş ilanlar */}
|
||||||
<label
|
<label
|
||||||
className="flex items-center gap-1.5 text-xs font-semibold text-slate-700 cursor-pointer select-none whitespace-nowrap"
|
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"
|
title="Süresi geçtiği bilinen, iptal edilen ve sonuçlanan kayıtlar gizlenir. Tarihi veya saati bilinmeyen kayıtlar ayrıca doğrulanmalıdır."
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -156,10 +156,10 @@ export const TenderList = () => {
|
|||||||
onChange={(e) => setOnlyValidOnly(e.target.checked)}
|
onChange={(e) => setOnlyValidOnly(e.target.checked)}
|
||||||
className="accent-blue-600"
|
className="accent-blue-600"
|
||||||
/>
|
/>
|
||||||
Sadece açık ihaleler
|
Süresi geçenleri gizle
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<select
|
<select aria-label="İhale sıralaması"
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
className="bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 text-xs font-semibold text-slate-700 focus:ring-2 focus:ring-blue-500"
|
className="bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 text-xs font-semibold text-slate-700 focus:ring-2 focus:ring-blue-500"
|
||||||
|
|||||||
+16
-13
@@ -1,3 +1,4 @@
|
|||||||
|
import { deadlineInfo, tenderDateKey, istanbulDateKey } from './tenderCalendar.js';
|
||||||
/**
|
/**
|
||||||
* İhale tarih yardımcıları — üst kartlar (SummaryCards) ile liste filtresi
|
* İhale tarih yardımcıları — üst kartlar (SummaryCards) ile liste filtresi
|
||||||
* (TenderList) aynı ölçütü kullansın diye tek yerde.
|
* (TenderList) aynı ölçütü kullansın diye tek yerde.
|
||||||
@@ -11,9 +12,8 @@
|
|||||||
// "gg.aa.yyyy" → Date. Ayrıştırılamayanı GEÇMİŞ SAYMAYIZ: tarihi belirsiz bir
|
// "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.
|
// ihaleyi sessizce gizlemek, geçmiş bir ihaleyi göstermekten daha kötüdür.
|
||||||
export const ihaleGunu = (v) => {
|
export const ihaleGunu = (v) => {
|
||||||
const m = /^(\d{2})\.(\d{2})\.(\d{4})/.exec(String(v || '').trim());
|
const key = tenderDateKey({tarih:v});
|
||||||
if (!m) return null;
|
return key ? new Date(key+'T00:00:00+03:00') : null;
|
||||||
return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Her biçimi "yyyy-aa-gg" dizesine indirger (karşılaştırma için).
|
// Her biçimi "yyyy-aa-gg" dizesine indirger (karşılaştırma için).
|
||||||
@@ -26,12 +26,7 @@ export const ilanTarihiISO = (t) => {
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const iso = (d) => {
|
const iso = istanbulDateKey;
|
||||||
const y = d.getFullYear();
|
|
||||||
const a = String(d.getMonth() + 1).padStart(2, '0');
|
|
||||||
const g = String(d.getDate()).padStart(2, '0');
|
|
||||||
return `${y}-${a}-${g}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bugunISO = () => iso(new Date());
|
export const bugunISO = () => iso(new Date());
|
||||||
|
|
||||||
@@ -47,10 +42,18 @@ export const girisSonrasiEklendi = (t, oncekiGirisMs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Süresi geçmemiş (veya tarihi okunamayan) ihale — listenin varsayılan görünümü.
|
// Süresi geçmemiş (veya tarihi okunamayan) ihale — listenin varsayılan görünümü.
|
||||||
export const acikIhale = (t, bugun) => {
|
export const acikIhale = (t, now = new Date()) =>
|
||||||
const gun = ihaleGunu(t?.tarih);
|
!['Sonuçlandı','Sözleşme İmzalandı','İptal'].includes(t?.durum) && !t?.kazananFirma && deadlineInfo(t,now).expired !== true;
|
||||||
return !(gun && gun < bugun);
|
|
||||||
};
|
export function compareTenderDeadlines(a,b,direction = 1) {
|
||||||
|
const x=deadlineInfo(a),y=deadlineInfo(b);
|
||||||
|
if (!x.dateKey || !y.dateKey) return x.dateKey ? -1 : y.dateKey ? 1 : 0;
|
||||||
|
const day=x.dateKey.localeCompare(y.dateKey);
|
||||||
|
if (day) return day * direction;
|
||||||
|
// Unknown times follow known times on the same day in either direction.
|
||||||
|
if (!x.deadline || !y.deadline) return x.deadline ? -1 : y.deadline ? 1 : 0;
|
||||||
|
return (x.deadline-y.deadline)*direction;
|
||||||
|
}
|
||||||
|
|
||||||
// Bu hızlı filtreler doğası gereği GEÇMİŞ ihaleleri hedefler; "sadece açık
|
// Bu hızlı filtreler doğası gereği GEÇMİŞ ihaleleri hedefler; "sadece açık
|
||||||
// ihaleler" kapısı bunlara uygulanmaz, yoksa hiç sonuç çıkmaz.
|
// ihaleler" kapısı bunlara uygulanmaz, yoksa hiç sonuç çıkmaz.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { acikIhale, compareTenderDeadlines, ihaleGunu } from '../src/services/ihaleTarih.js';
|
||||||
|
test('list date sorting crosses month/year boundaries and leaves unknown dates last',()=>{
|
||||||
|
const rows=[
|
||||||
|
{id:'oct',tarih:'01.10.2026',saat:'09:00'},
|
||||||
|
{id:'sep-late',tarih:'30.09.2026',saat:'14:00'},
|
||||||
|
{id:'sep-early',tarih:'30.09.2026',saat:'09:00'},
|
||||||
|
{id:'year',tarih:'01.01.2027',saat:'09:00'},
|
||||||
|
{id:'unknown',tarih:''},
|
||||||
|
];
|
||||||
|
assert.deepEqual([...rows].sort(compareTenderDeadlines).map(t=>t.id),['sep-early','sep-late','oct','year','unknown']);
|
||||||
|
assert.deepEqual([...rows].sort((a,b)=>compareTenderDeadlines(a,b,-1)).map(t=>t.id),['year','oct','sep-late','sep-early','unknown']);
|
||||||
|
assert.equal(ihaleGunu('31.09.2026'),null);
|
||||||
|
});
|
||||||
|
test('open list closes at exact deadline but never invents an unknown time',()=>{
|
||||||
|
const t={tarih:'06.09.2026',saat:'10:00'};
|
||||||
|
assert.equal(acikIhale(t,new Date('2026-09-06T06:59:59Z')),true);
|
||||||
|
assert.equal(acikIhale(t,new Date('2026-09-06T07:00:00Z')),false);
|
||||||
|
assert.equal(acikIhale({...t,saat:''},new Date('2026-09-06T20:59:59Z')),true);
|
||||||
|
assert.equal(acikIhale({...t,saat:''},new Date('2026-09-06T21:00:00Z')),false);
|
||||||
|
for(const durum of ['İptal','Sonuçlandı','Sözleşme İmzalandı']) assert.equal(acikIhale({...t,durum},new Date('2026-09-05')),false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user