diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index cbb91bd..d3e4c1d 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -997,3 +997,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Hata listesi e-posta alanına yönlendirir; alan altında açıklama, aria-invalid ve aria-describedby bağlantısı bulunur. E-posta ve telefon alanları uygun HTML input türlerini kullanır. Geçersiz taslak yine kaydedilebilir; hazır çıktı kapısı açılmaz. - 226 otomatik test ve derleme geçti. Hatalı adresler, baş/son boşluklar, opsiyonel sicil alanı, zorunlu alanlarda nesne/dizi/boşluk ve hazırlık özetiyle aynı hata hedefi sınandı. - Bu biçim/tamlık kontrolüdür; vergi kaydı, yetki, telefonun ulaşılabilirliği veya e-posta sahipliği doğrulaması değildir. Gerçek firma uygunluğu ve yeni alan hata görünümünün tarayıcı kabulü açık. Canlıya yayın yapılmadı. + +### Takvim gündeminde saat sırası ve belirsiz tarih görünümü — 6 Eylül 2026 +- Aynı gündeki ihaleler artık bilinen son teklif saatine göre artan sırada; saati bilinmeyenler sonda gösterilir. Eşit saatlerde İKN/kimlik sırası kullanılır. Sayımlar için havuz her gün yeniden taranmak yerine tek geçişte günlere ayrılır. +- Tarihi eksik veya geçersiz kayıtlar önceden takvimde tamamen kayboluyordu. Tarihi doğrulanacak ihaleler bölümünde kaynak detayına erişilebilir biçimde gösterilir; bunlara gün/saat uydurulmaz. Mevcut iptal/sonuç durumu ve son teklif zamanı etiketi kartta görünür. +- Gün seçiminde aria-pressed ve tam tarihli erişilebilir ad eklendi. İhale kartları klavye davranışı elle yazılmış div yerine doğal button oldu. Mevcut ay seçicisi korunur. +- 227 otomatik test ve derleme geçti. Karışık saat sırası, bilinmeyen saat, geçersiz/eksik tarihin kaybolmaması, geçmiş kayıt ve girdi sırasının değiştirilmemesi test edildi. +- Yeni takvim kartlarının tarayıcı/mobil kabulü ve Google/Outlook aktarım kabulü açık. Bu değişiklik ICS aktarımını test etmiş sayılmaz. Canlıya yayın yapılmadı. diff --git a/src/components/tools/TenderCalendar.jsx b/src/components/tools/TenderCalendar.jsx index 81d851e..fd276b3 100644 --- a/src/components/tools/TenderCalendar.jsx +++ b/src/components/tools/TenderCalendar.jsx @@ -1,8 +1,8 @@ import { useDeadlineClock } from '../../hooks/useDeadlineClock'; -import { calendarMonthDays, istanbulDateKey, tenderDateKey, deadlineInfo } from '../../services/tenderCalendar'; -import React, { useState } from 'react'; +import { calendarMonthDays, istanbulDateKey, calendarAgenda } from '../../services/tenderCalendar'; +import React, { useState, useMemo } from 'react'; import { useApp } from '../../context/AppContext'; -import { CalendarDays, Clock, MapPin, AlertCircle } from 'lucide-react'; +import { CalendarDays } from 'lucide-react'; export const TenderCalendar = () => { const now = useDeadlineClock(); @@ -10,8 +10,22 @@ export const TenderCalendar = () => { const today = istanbulDateKey(now); const [month,setMonth] = useState(today.slice(0,7)); const [selectedDay, setSelectedDay] = useState(today); - const daysList = calendarMonthDays(month).map(day => ({...day,count:tenders.filter(t=>tenderDateKey(t)===day.key).length})); - const dayTenders = tenders.filter(t => tenderDateKey(t) === selectedDay); + const agenda=useMemo(()=>calendarAgenda(tenders,now),[tenders,now]); + const daysList=calendarMonthDays(month).map(day=>({...day,count:agenda.days.get(day.key)?.length || 0})); + const dayTenders=agenda.days.get(selectedDay) || []; + const card=({tender:t,info})=>; return (
@@ -38,6 +52,8 @@ export const TenderCalendar = () => { {daysList.map(d => (
); }; diff --git a/src/services/tenderCalendar.js b/src/services/tenderCalendar.js index d0ca444..d24870c 100644 --- a/src/services/tenderCalendar.js +++ b/src/services/tenderCalendar.js @@ -65,3 +65,24 @@ export function calendarMonthDays(month) { label:new Date(`${month}-${day}T12:00:00Z`).toLocaleDateString('tr-TR',{weekday:'short',timeZone:'Europe/Istanbul'})}; }); } + +/** Group once and order each date by known deadline; undated records remain visible. */ +export function calendarAgenda(tenders, now=new Date()) { + const days=new Map(),undated=[]; + for(const tender of tenders){ + const entry={tender,info:deadlineInfo(tender,now)}; + if(!entry.info.dateKey){undated.push(entry);continue;} + if(!days.has(entry.info.dateKey))days.set(entry.info.dateKey,[]); + days.get(entry.info.dateKey).push(entry); + } + const compare=(a,b)=>{ + const aTime=a.info.deadline?.getTime(),bTime=b.info.deadline?.getTime(); + if(aTime!==undefined&&bTime===undefined)return -1; + if(aTime===undefined&&bTime!==undefined)return 1; + if(aTime!==undefined&&aTime!==bTime)return aTime-bTime; + return String(a.tender.ikn || a.tender.id || '').localeCompare(String(b.tender.ikn || b.tender.id || ''),'tr'); + }; + for(const entries of days.values())entries.sort(compare); + undated.sort(compare); + return {days,undated}; +} diff --git a/tests/calendar.test.mjs b/tests/calendar.test.mjs index d169271..bbe82f9 100644 --- a/tests/calendar.test.mjs +++ b/tests/calendar.test.mjs @@ -60,3 +60,22 @@ test('deadline ICS is a transparent instant, preserves UID across date changes a assert.equal(hostile.split('\r\n').filter(line=>line==='BEGIN:VEVENT').length,1); assert.equal(hostile.split('\r\n').filter(line=>line==='END:VEVENT').length,1); }); + +test('calendar agenda sorts known times before unknown and preserves undated records',async()=>{ + const {calendarAgenda}=await import('../src/services/tenderCalendar.js'); + const now=new Date('2026-09-06T08:00:00Z'); + const records=[ + {id:'late',tarih:'10.09.2026',saat:'16:00'}, + {id:'unknown',tarih:'10.09.2026'}, + {id:'early',tarih:'10.09.2026',saat:'09:00'}, + {id:'bad',tarih:'31.02.2026'}, + {id:'missing'}, + {id:'old',tarih:'01.09.2026',saat:'09:00',durum:'İptal'}, + ]; + const agenda=calendarAgenda(records,now); + assert.deepEqual(agenda.days.get('2026-09-10').map(e=>e.tender.id),['early','late','unknown']); + assert.deepEqual(agenda.undated.map(e=>e.tender.id),['bad','missing']); + assert.equal(agenda.days.get('2026-09-01')[0].info.expired,true); + assert.equal([...agenda.days.values()].flat().length+agenda.undated.length,records.length); + assert.equal(records[0].id,'late'); +});