const norm = v => String(v || '').trim().replace(/\s+/g,' ').toLocaleLowerCase('tr-TR'); export function tenderIdentity(t) { const ikn = String(t.ikn || '').replace(/\s/g,''); // Awards from different lots must never be collapsed into one contract. if (norm(t.durum) === 'sonuçlandı') return `award:${ikn}:${t.kisim ?? t.id}:${norm(t.kazananFirma)}`; if (/^\d{2}DT\d+$/i.test(ikn)) return `dt:${ikn.toUpperCase()}:${norm(t.kurum)}`; // Corrections/cancellations remain separate events until their source linkage is verified. if (/düzeltme|zeyilname|iptal/i.test(t.baslik || '')) return `event:${t.id}`; return /^\d{4}\/\d+$/.test(ikn) ? `tender:${ikn}` : `source:${t.id}`; } export function uniqueTenders(records) { const found = new Map(); for (const t of records) { // Kaynaklardan gelen bozuk/null satırlar tüm havuz birleştirmesini düşürmemeli. if (!t || typeof t !== 'object' || Array.isArray(t) || typeof t.id !== 'string' || !t.id.trim()) continue; const key = tenderIdentity(t), old = found.get(key); if (!old) { found.set(key,{...t,sourceIds:[t.id]}); continue; } const sourceIds = [...new Set([...old.sourceIds,t.id])]; const merged = {...old}; for (const [field,value] of Object.entries(t)) { if (merged[field] == null || merged[field] === '' || (Array.isArray(merged[field]) && !merged[field].length)) merged[field] = value; } if ((t.aciklama || '').length > (old.aciklama || '').length) merged.aciklama = t.aciklama; found.set(key,{...merged,id:old.id,sourceIds}); } return [...found.values()]; } export function announcementFields(text) { const content = String(text || ''); const result = {aciklama:content}; // EKAP table cells are separated by pipes in extracted text. Keep the // original announcement intact; normalize separators only for field parsing. const fields = content.replace(/\s*\|\s*/g, ' '); // EKAP/ilan kaynakları aynı son teklif anını farklı başlıklarla verir. // Yalnız tarih + saat birlikte bulunduğunda alan üret; teslim süresi gibi // yalnızca tarih içeren maddeleri son teklif tarihi sanma. const deadlinePatterns = [ /Tarih\s*ve\s*Saati\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i, /(?:İhale\s+|Son\s+Teklif\s+)?Tarih(?:i)?\s*(?:\/\s*Saat(?:i)?|ve\s+Saat(?:i)?|\s+Saat(?:i)?)\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i, /Son\s+Teklif\s+Tarihi\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i, ]; const deadline = deadlinePatterns.map(pattern => pattern.exec(fields)).find(Boolean); if (deadline) { result.tarih = deadline[1]; result.saat = deadline[2]; } const place = /3\.3\.?\s*[^:]{0,120}:\s*([\s\S]*?)(?=3\.4\.?)/i.exec(fields); const duration = /3\.4\.?\s*[^:]{0,100}:\s*([\s\S]*?)(?=3\.5\.?|4\s*[-–])/i.exec(fields); if (place) result.teslimYeri = place[1].trim(); if (duration) result.teslimatSuresi = duration[1].trim(); if (/hizmet alımı\s+4734/i.test(content)) result.alimTuru = 'Kiralama ve Hizmet Alımı'; return result; } /** * EKAP ihale detayındaki tarih-saat alanını liste ve takvim alanlarına ayırır. * Ayrıştırılamayan/eksik değerler için varsayılan tarih veya saat üretmez. */ export function officialDateFields(value) { const raw = String(value || '').trim(); if (!raw) return {}; const dotted = raw.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(?:[-–T]|\s)\s*(\d{1,2}):(\d{2})/); if (dotted) return { tarih: `${dotted[1]}.${dotted[2]}.${dotted[3]}`, saat: `${dotted[4].padStart(2, '0')}:${dotted[5]}`, sourceDateTime: raw, }; const iso = raw.match(/(\d{4})-(\d{2})-(\d{2})[T\s]+(\d{1,2}):(\d{2})/); if (iso) return { tarih: `${iso[3]}.${iso[2]}.${iso[1]}`, saat: `${iso[4].padStart(2, '0')}:${iso[5]}`, sourceDateTime: raw, }; return { sourceDateTime: raw }; } /** * İlan metninden çıkarılan alanları, resmî EKAP ihale detayındaki alanlarla * birleştirir. Resmî detay tarihi ilan HTML'inden eski/çelişkili bir tarih * gelirse üstün tutulur; fark kullanıcıya açık bir uyarı olarak taşınır. */ export function mergeAnnouncementFields(text, authoritative = {}) { const parsed = announcementFields(text); const sourceTarih = String(authoritative.tarih || '').trim(); const sourceSaat = String(authoritative.saat || '').trim(); const dateConflict = Boolean( (sourceTarih && parsed.tarih && sourceTarih !== parsed.tarih) || (sourceSaat && parsed.saat && sourceSaat !== parsed.saat), ); const merged = { ...parsed, ...(sourceTarih ? { tarih: sourceTarih } : {}), ...(sourceSaat ? { saat: sourceSaat } : {}), ...(authoritative.sourceDateTime ? { sourceDateTime: authoritative.sourceDateTime } : {}), }; if (dateConflict) { merged.sourceDateWarning = 'Resmî ihale detay tarihi ile ilan metnindeki tarih farklı; güncel detay tarihi esas alındı.'; } return merged; }