fix: order new tenders by validated publication time
This commit is contained in:
@@ -228,3 +228,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
|
|||||||
- Türkçe metin, virgül, satır sonu, #, & ve İKN metin olarak korunuyor. Kaynak metni Excel formülüne dönüştürülmüyor. Başlık sabitleme ve otomatik filtre var.
|
- Türkçe metin, virgül, satır sonu, #, & ve İKN metin olarak korunuyor. Kaynak metni Excel formülüne dönüştürülmüyor. Başlık sabitleme ve otomatik filtre var.
|
||||||
- Çıktı hazırlanırken düğme bekleme gösteriyor; hata görünür. Ekran kapatılır veya oturum değişirse geç gelen dosya indirmesi iptal ediliyor. Boş liste dışa aktarımı engelleniyor.
|
- Çıktı hazırlanırken düğme bekleme gösteriyor; hata görünür. Ekran kapatılır veya oturum değişirse geç gelen dosya indirmesi iptal ediliyor. Boş liste dışa aktarımı engelleniyor.
|
||||||
- Kanıt: 125 satırın gerçek XLSX yeniden okunması, ilk/son kayıt, saat, CPV, kaynak ve = ile başlayan metin hücresi testi geçti. 62/62 test ve build geçti. Tarayıcıdan dosya indirme ve masaüstü Excel açılışı bu adımda yapılmadı; tüm çıktı kabulü henüz tamamlanmış sayılmıyor.
|
- Kanıt: 125 satırın gerçek XLSX yeniden okunması, ilk/son kayıt, saat, CPV, kaynak ve = ile başlayan metin hücresi testi geçti. 62/62 test ve build geçti. Tarayıcıdan dosya indirme ve masaüstü Excel açılışı bu adımda yapılmadı; tüm çıktı kabulü henüz tamamlanmış sayılmıyor.
|
||||||
|
|
||||||
|
### D05 — Yayın zamanına göre yeni ilan sıralaması
|
||||||
|
|
||||||
|
- “En yeni” artık kayıt kimliğine göre değil doğrulanabilir yayın tarihi/zamanına göre sıralanıyor. Alan etiketi “Yayın Tarihi: En Yeni” olarak açıklandı. Tarihi bilinmeyenler sonda; aynı günde saati bilinmeyenler kesin saatli kayıtların ardından geliyor.
|
||||||
|
- ISO/ofsetli zamanlar Türkiye gününe dönüştürülüyor; Türkçe kaynaklardaki ofsetsiz saatler İstanbul kabul ediliyor. Geçersiz takvim günü/saat reddediliyor; varsa ayrı ilan tarihi kullanılıyor. Süre metni veya keyfi kayıt kimliği yayın zamanı olarak yorumlanmıyor.
|
||||||
|
- Bugün eklenenler hesabında bilinen yayın tarihi, eskimiş isYeni bayrağına üstün geliyor. Uyum puanı eşitliğinde de kimlik yerine yayın sırası kullanılıyor; sayısal id nedeniyle localeCompare hatası ortadan kalktı.
|
||||||
|
- Kanıt: yayın zamanı regresyonuyla 63/63 test ve build geçti. UTC/Türkiye gün sınırı, ofsetsiz saat, farklı türde kimlik, geçersiz tarih ve eski isYeni bayrağı doğrulandı. Kaynakların gerçek saat dilimi sözleşmelerinin tek tek teyidi ve gerçek veriyle görsel kabul hâlâ açık.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react';
|
|||||||
import { createTenderWorkbook } from '../services/tenderWorkbook';
|
import { createTenderWorkbook } from '../services/tenderWorkbook';
|
||||||
import { getToken } from '../services/auth';
|
import { getToken } from '../services/auth';
|
||||||
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
import { useDeadlineClock } from '../hooks/useDeadlineClock';
|
||||||
import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER, compareTenderDeadlines } from '../services/ihaleTarih';
|
import { acikIhale, hizliFiltreGecer, GECMIS_FILTRELER, compareTenderDeadlines, compareTenderPublications } 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.
|
||||||
@@ -72,8 +72,8 @@ export const TenderList = () => {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}).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) || compareTenderPublications(a,b);
|
||||||
if (sortBy === 'newest') return b.id.localeCompare(a.id);
|
if (sortBy === 'newest') return compareTenderPublications(a,b);
|
||||||
if (sortBy === 'urgent') return compareTenderDeadlines(a,b);
|
if (sortBy === 'urgent') return compareTenderDeadlines(a,b);
|
||||||
if (sortBy === 'distant') return compareTenderDeadlines(a,b,-1);
|
if (sortBy === 'distant') return compareTenderDeadlines(a,b,-1);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -164,7 +164,7 @@ export const TenderList = () => {
|
|||||||
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"
|
||||||
>
|
>
|
||||||
<option value="uyum">Şirketime En Uyumlu</option>
|
<option value="uyum">Şirketime En Uyumlu</option>
|
||||||
<option value="newest">En Yeni Eklenenler</option>
|
<option value="newest">Yayın Tarihi: En Yeni</option>
|
||||||
<option value="urgent">Tarihi Yaklaşanlar (Acil)</option>
|
<option value="urgent">Tarihi Yaklaşanlar (Acil)</option>
|
||||||
<option value="distant">Tarihi Uzak Olanlar</option>
|
<option value="distant">Tarihi Uzak Olanlar</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -179,6 +179,7 @@ export const TenderList = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sortBy === 'newest' && <p className="text-xs text-slate-500">Yayın tarihi bilinmeyen kayıtlar sonda gösterilir. Aynı gün içinde saati bilinen kayıtlar önce gelir.</p>}
|
||||||
{exportError && <p role="alert" className="text-sm text-red-700">{exportError}</p>}
|
{exportError && <p role="alert" className="text-sm text-red-700">{exportError}</p>}
|
||||||
{/* Tender Cards List */}
|
{/* Tender Cards List */}
|
||||||
{gorunenTenders.length === 0 ? (
|
{gorunenTenders.length === 0 ? (
|
||||||
|
|||||||
+31
-12
@@ -16,22 +16,41 @@ export const ihaleGunu = (v) => {
|
|||||||
return key ? new Date(key+'T00:00:00+03:00') : null;
|
return key ? new Date(key+'T00:00:00+03:00') : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Her biçimi "yyyy-aa-gg" dizesine indirger (karşılaştırma için).
|
// Publication metadata is distinct from the tender deadline.
|
||||||
export const ilanTarihiISO = (t) => {
|
export function publicationInfo(t) {
|
||||||
const v = String(t?.ilanTarihi || '').trim();
|
for (const value of [t?.yayinZamani,t?.ilanTarihi]) {
|
||||||
let m = /^(\d{4})-(\d{2})-(\d{2})/.exec(v);
|
const v=String(value || '').trim();
|
||||||
if (m) return `${m[1]}-${m[2]}-${m[3]}`;
|
const m=/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,7}))?)?(Z|[+-]\d{2}:\d{2})?)?$/.exec(v);
|
||||||
m = /^(\d{2})\.(\d{2})\.(\d{4})/.exec(v);
|
const tr=/^(\d{2})\.(\d{2})\.(\d{4})$/.exec(v);
|
||||||
if (m) return `${m[3]}-${m[2]}-${m[1]}`;
|
const key=m ? tenderDateKey({tarih:`${m[3]}.${m[2]}.${m[1]}`}) : tr ? tenderDateKey({tarih:v}) : null;
|
||||||
return '';
|
if (!key) continue;
|
||||||
};
|
if (!m?.[4]) return {dateKey:key,timestamp:null};
|
||||||
|
if (+m[4]>23 || +m[5]>59 || +(m[6] || 0)>59) continue;
|
||||||
|
// Offset-free Turkish source timestamps use Istanbul, never the browser timezone.
|
||||||
|
const stamp=Date.parse(`${key}T${m[4]}:${m[5]}:${m[6] || '00'}${m[7] ? '.'+m[7].slice(0,3) : ''}${m[8] || '+03:00'}`);
|
||||||
|
if (Number.isFinite(stamp)) return {dateKey:istanbulDateKey(new Date(stamp)),timestamp:stamp};
|
||||||
|
}
|
||||||
|
return {dateKey:null,timestamp:null};
|
||||||
|
}
|
||||||
|
export const ilanTarihiISO = t => publicationInfo(t).dateKey || '';
|
||||||
|
export function compareTenderPublications(a,b) {
|
||||||
|
const x=publicationInfo(a),y=publicationInfo(b);
|
||||||
|
if (!x.dateKey || !y.dateKey) return x.dateKey ? -1 : y.dateKey ? 1 : 0;
|
||||||
|
const day=y.dateKey.localeCompare(x.dateKey);
|
||||||
|
if (day) return day;
|
||||||
|
if (x.timestamp === null || y.timestamp === null) return x.timestamp !== null ? -1 : y.timestamp !== null ? 1 : 0;
|
||||||
|
return y.timestamp-x.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
const iso = istanbulDateKey;
|
const iso = istanbulDateKey;
|
||||||
|
|
||||||
export const bugunISO = () => iso(new Date());
|
export const bugunISO = (now = new Date()) => iso(now);
|
||||||
|
|
||||||
// Bugün yayımlanan ilan. Sunucunun isYeni bayrağı esas; o yoksa ilan tarihine bakılır.
|
// Bugün yayımlanan ilan. Bilinen yayın tarihi esas; yalnız tarih yoksa kaynak bayrağı kullanılır.
|
||||||
export const bugunEklendi = (t) => Boolean(t?.isYeni) || (ilanTarihiISO(t) !== '' && ilanTarihiISO(t) === bugunISO());
|
export const bugunEklendi = (t, now = new Date()) => {
|
||||||
|
const day=ilanTarihiISO(t);
|
||||||
|
return day ? day === bugunISO(now) : Boolean(t?.isYeni);
|
||||||
|
};
|
||||||
|
|
||||||
// Önceki ziyaretten (ms) sonra yayımlanan ilan. Kayıt yoksa "bugün" kabul edilir.
|
// Önceki ziyaretten (ms) sonra yayımlanan ilan. Kayıt yoksa "bugün" kabul edilir.
|
||||||
export const girisSonrasiEklendi = (t, oncekiGirisMs) => {
|
export const girisSonrasiEklendi = (t, oncekiGirisMs) => {
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { acikIhale, compareTenderDeadlines, ihaleGunu } from '../src/services/ihaleTarih.js';
|
import { acikIhale, compareTenderDeadlines, ihaleGunu, compareTenderPublications, publicationInfo, bugunEklendi } from '../src/services/ihaleTarih.js';
|
||||||
|
test('publication sorting ignores arbitrary ids and handles Istanbul day boundaries',()=>{
|
||||||
|
const rows=[{id:'zzz',ilanTarihi:'05.09.2026'}, {id:1,yayinZamani:'2026-09-05T22:00:00Z'},
|
||||||
|
{id:'aaa',yayinZamani:'2026-09-06T09:00:00'}, {id:'unknown'}];
|
||||||
|
assert.deepEqual([...rows].sort(compareTenderPublications).map(t=>t.id),['aaa',1,'zzz','unknown']);
|
||||||
|
assert.deepEqual(publicationInfo(rows[1]),{dateKey:'2026-09-06',timestamp:Date.parse('2026-09-05T22:00:00Z')});
|
||||||
|
assert.equal(publicationInfo(rows[2]).timestamp,Date.parse('2026-09-06T06:00:00Z'));
|
||||||
|
assert.equal(publicationInfo({ilanTarihi:'2026-02-31'}).dateKey,null);
|
||||||
|
assert.equal(publicationInfo({yayinZamani:'2026-09-06T25:00:00',ilanTarihi:'05.09.2026'}).dateKey,'2026-09-05');
|
||||||
|
assert.equal(bugunEklendi({ilanTarihi:'05.09.2026',isYeni:true},new Date('2026-09-06T09:00:00Z')),false);
|
||||||
|
});
|
||||||
test('list date sorting crosses month/year boundaries and leaves unknown dates last',()=>{
|
test('list date sorting crosses month/year boundaries and leaves unknown dates last',()=>{
|
||||||
const rows=[
|
const rows=[
|
||||||
{id:'oct',tarih:'01.10.2026',saat:'09:00'},
|
{id:'oct',tarih:'01.10.2026',saat:'09:00'},
|
||||||
|
|||||||
Reference in New Issue
Block a user