From a0f4824c7e9010d5c1ff12d0cf79167383f9de4b Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:47:07 +0300 Subject: [PATCH] Show tender source changes against saved preparation metadata --- docs/quality/uygulama-durumu.md | 8 ++++++++ src/components/TenderDetailModal.jsx | 5 +++++ src/context/AppContext.jsx | 3 ++- src/services/bidSource.js | 1 + src/services/tenderState.js | 12 ++++++++++-- tests/tender-source-changes.test.mjs | 20 ++++++++++++++++++++ 6 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/tender-source-changes.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 130701b..913e1ba 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -663,3 +663,11 @@ Migration sayısı 5 oldu. Testler: DELETE trigger hatasında gerçek dosya bayt Her durumda ayrı yeni süreç kullanıcı deposunu açtı. Commit öncesi kesintide kayıt ve özgün Türkçe dosya baytları korundu; commit sonrası kesintide kayıt yoktu ve kalıcı temizleme görevi yeni süreçte dosyayı kaldırdı. İki durumda da PRAGMA integrity_check sonucu ok, kuyruk sıfırdı. Bu, uygulama süreci kesilmesinin kabulüdür; donanım güç kaybı/dosya sistemi arızası veya bütün production rollback kabulü değildir. Tüm 142 test geçti. Üretim kodu değişmediği için derleme tekrarlanmadı. D01'in bu kesinti maddesi kanıtlandı; diğer plan maddeleri ve canlı yayın açık. + +### D04 / D05 — Son kayda göre kaynak değişikliği uyarısı + +Takip/hazırlık kaydıyla saklanan ihale özetine durum ve doküman sürümü eklendi. Canlı havuz verisi veya ayrıntı metni geldiğinde son kaydedilen özetle tarih, saat, durum, başlık, idare, alım türü ve sürüm karşılaştırılır. Bilinmeyen eski alanlardan değişiklik uydurulmaz. Ayrıntı ekranı değişen alanların önce/sonra değerlerini ve kayıtların korunduğunu gösterir. + +Kullanıcı durumu İKN üzerinden birleştirilir; kaynak kimliği değişse de not ve fiyat taslağı korunur. İhale durumunun değişmesi hazırlık kaynak onayını da geçersizleştirir. Bu uyarı son kaydedilen bilgilere göre karşılaştırmadır; tüm zeyilname olaylarının kalıcı geçmişi değildir. Yeni kayıtta karşılaştırma tabanı güncellenir. + +Test: tarih/saat/iptal/sürüm değişiminde doğru dört fark; değişen kaynak id'sine rağmen kayıtların korunması; eski bilinmeyen alanlarda yanlış uyarı olmaması; yeni tabanla farkın temizlenmesi. Tüm 144 test ve üretim derlemesi geçti; ayrıntı getirme bağlantısı sonrasında derleme yeniden doğrulandı. Yeni uyarının tarayıcı kabulü ve tam kaynak sürüm arşivi açık. Canlı yayın yapılmadı. diff --git a/src/components/TenderDetailModal.jsx b/src/components/TenderDetailModal.jsx index c8ac4c2..7ca6abc 100644 --- a/src/components/TenderDetailModal.jsx +++ b/src/components/TenderDetailModal.jsx @@ -105,6 +105,11 @@ export const TenderDetailModal = () => {
{(actionError || saveError) &&

{actionError || saveError}

} + {tender.sourceChanges?.length > 0 &&
+

Son kaydettiğiniz bilgilere göre ihale değişmiş

+
    {tender.sourceChanges.map(change =>
  • {change.label}: {change.before} → {change.after}
  • )}
+

Takip, not ve hazırlık kayıtlarınız korundu. Yeni koşulları resmi kaynakla karşılaştırın.

+
} {tender.sourceWarning &&

{tender.sourceWarning}

} {detailLoad?.id === tender.id && detailLoad.status === 'loading' &&

Kaynak ilan metni yükleniyor…

} {detailLoad?.id === tender.id && detailLoad.status === 'error' &&
diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx index 7c413e8..3aed0e2 100644 --- a/src/context/AppContext.jsx +++ b/src/context/AppContext.jsx @@ -7,7 +7,7 @@ import { getToken } from '../services/auth'; import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi'; import { restoreSession, clearSession, getStoredUser } from '../services/auth'; import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account'; -import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender } from '../services/tenderState'; +import { applyTenderStates, tenderStateKey, tenderSnapshot, publicTender, tenderSourceChanges } from '../services/tenderState'; import { getTenderStates, saveTenderState } from '../services/account'; import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants'; @@ -310,6 +310,7 @@ export const AppProvider = ({ children }) => { .then(detail => { if (cancelled) return; const source = publicTender(detail); + source.sourceChanges = tenderSourceChanges(savedTenderStates.current[tenderStateKey(t)]?.snapshot, {...t,...source}); setDetailLoad({id:t.id,status:'loaded'}); setActiveTenderDetail(prev => prev && prev.id === t.id ? { ...prev, ...source } : prev diff --git a/src/services/bidSource.js b/src/services/bidSource.js index c989020..cf5bcf1 100644 --- a/src/services/bidSource.js +++ b/src/services/bidSource.js @@ -32,6 +32,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res tender: tender?.ikn || tender?.id, announcement: tender?.aciklama || '', deadline: [tender?.tarih || '', tender?.saat || ''], + status: tender?.durum || '', documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null, procurementTerms: results?.procurementTerms || null, analysis: preparationAnalysisSource(results), diff --git a/src/services/tenderState.js b/src/services/tenderState.js index dc10c0f..63af52c 100644 --- a/src/services/tenderState.js +++ b/src/services/tenderState.js @@ -1,11 +1,19 @@ export const tenderStateKey = t => String(t?.ikn || t?.id || '').trim(); +export const TENDER_SOURCE_FIELDS = [['tarih','İhale tarihi'],['saat','İhale saati'],['durum','İhale durumu'],['baslik','İhale başlığı'],['kurum','İdare'],['alimTuru','Alım türü'],['documentVersion','Doküman sürümü']]; +export function tenderSourceChanges(previous = {}, current = {}) { + return TENDER_SOURCE_FIELDS.flatMap(([field,label]) => { + const before = previous[field], after = current[field]; + if (before == null || before === '' || after == null || after === '' || String(before) === String(after)) return []; + return [{field,label,before:String(before),after:String(after)}]; + }); +} export const USER_STATE_FIELDS = ['takipte', 'incelendi', 'kanbanStatus', 'atananPersonel', 'gorev', 'notes', 'checklist', 'bidDraft']; export function applyTenderStates(tenders, records = {}) { const seen = new Set(); const result = tenders.map(t => { const key = tenderStateKey(t); seen.add(key); const record = records[key]; - return record ? { ...t, ...record.data } : t; + return record ? { ...t, ...record.data, sourceChanges:tenderSourceChanges(record.snapshot,t) } : t; }); for (const [key, record] of Object.entries(records)) { if (!seen.has(key) && record.snapshot && (record.data.takipte || record.data.gorev || record.data.incelendi || record.data.bidDraft || record.data.notes?.length)) @@ -14,7 +22,7 @@ export function applyTenderStates(tenders, records = {}) { return result; } export function tenderSnapshot(t) { - return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru'].map(k => [k,t[k] ?? ''])); + return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru','durum','documentVersion'].map(k => [k,t[k] ?? ''])); } export function publicTender(tender) { diff --git a/tests/tender-source-changes.test.mjs b/tests/tender-source-changes.test.mjs new file mode 100644 index 0000000..cbc463f --- /dev/null +++ b/tests/tender-source-changes.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {applyTenderStates,tenderSnapshot,tenderSourceChanges} from '../src/services/tenderState.js'; +import {bidSourceSnapshot} from '../src/services/bidSource.js'; +test('fresh deadline and cancellation changes remain visible without overwriting saved work',()=>{ + const original={id:'old-id',ikn:'2026/1',tarih:'10.09.2026',saat:'10:00',durum:'Açık',documentVersion:'v1'}; + const current={...original,id:'new-source-id',tarih:'12.09.2026',saat:'11:00',durum:'İptal',documentVersion:'v2'}; + const data={takipte:true,notes:[{text:'Firma notu'}],bidDraft:{pricesBySource:{kit:'100'}}}; + const result=applyTenderStates([current],{'2026/1':{snapshot:tenderSnapshot(original),data}})[0]; + assert.equal(result.id,'new-source-id');assert.equal(result.durum,'İptal'); + assert.deepEqual(result.notes,data.notes);assert.deepEqual(result.bidDraft,data.bidDraft); + assert.deepEqual(result.sourceChanges.map(x=>x.field),['tarih','saat','durum','documentVersion']); + assert.notEqual(bidSourceSnapshot(original),bidSourceSnapshot({...original,durum:'İptal'})); +}); +test('unknown legacy fields do not invent a change and a new saved baseline clears comparisons',()=>{ + assert.deepEqual(tenderSourceChanges({tarih:''},{tarih:'12.09.2026',durum:'Açık'}),[]); + const current={ikn:'2026/2',tarih:'12.09.2026',saat:'11:00',durum:'Açık'}; + const result=applyTenderStates([current],{'2026/2':{snapshot:tenderSnapshot(current),data:{takipte:true}}}); + assert.deepEqual(result[0].sourceChanges,[]); +});