Show tender source changes against saved preparation metadata

This commit is contained in:
2026-09-06 10:47:07 +03:00
parent f51f9b4eb0
commit a0f4824c7e
6 changed files with 46 additions and 3 deletions
+8
View File
@@ -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ı.
+5
View File
@@ -105,6 +105,11 @@ export const TenderDetailModal = () => {
<div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={dialogTitleId} tabIndex={-1} onKeyDown={handleDialogKey} className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
{(actionError || saveError) && <p role="alert" className="p-3 bg-red-50 text-red-900">{actionError || saveError}</p>}
{tender.sourceChanges?.length > 0 && <div role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">
<p className="font-bold">Son kaydettiğiniz bilgilere göre ihale değişmiş</p>
<ul className="list-disc pl-5">{tender.sourceChanges.map(change => <li key={change.field}>{change.label}: {change.before} → {change.after}</li>)}</ul>
<p>Takip, not ve hazırlık kayıtlarınız korundu. Yeni koşulları resmi kaynakla karşılaştırın.</p>
</div>}
{tender.sourceWarning && <p role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">{tender.sourceWarning}</p>}
{detailLoad?.id === tender.id && detailLoad.status === 'loading' && <p role="status" className="p-3 bg-blue-50 text-blue-900">Kaynak ilan metni yükleniyor…</p>}
{detailLoad?.id === tender.id && detailLoad.status === 'error' && <div role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">
+2 -1
View File
@@ -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
+1
View File
@@ -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),
+10 -2
View File
@@ -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) {
+20
View File
@@ -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,[]);
});