Show tender source changes against saved preparation metadata
This commit is contained in:
@@ -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.
|
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.
|
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ı.
|
||||||
|
|||||||
@@ -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]">
|
<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>}
|
{(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>}
|
{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 === '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">
|
{detailLoad?.id === tender.id && detailLoad.status === 'error' && <div role="alert" className="p-3 bg-amber-50 text-amber-900 text-sm">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getToken } from '../services/auth';
|
|||||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||||
import { getAllUserData, setUserData, getPlan, bildirimleriGetir, bildirimOkundu, listDocuments } from '../services/account';
|
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 { getTenderStates, saveTenderState } from '../services/account';
|
||||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||||
|
|
||||||
@@ -310,6 +310,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
.then(detail => {
|
.then(detail => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const source = publicTender(detail);
|
const source = publicTender(detail);
|
||||||
|
source.sourceChanges = tenderSourceChanges(savedTenderStates.current[tenderStateKey(t)]?.snapshot, {...t,...source});
|
||||||
setDetailLoad({id:t.id,status:'loaded'});
|
setDetailLoad({id:t.id,status:'loaded'});
|
||||||
setActiveTenderDetail(prev =>
|
setActiveTenderDetail(prev =>
|
||||||
prev && prev.id === t.id ? { ...prev, ...source } : prev
|
prev && prev.id === t.id ? { ...prev, ...source } : prev
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
|
|||||||
tender: tender?.ikn || tender?.id,
|
tender: tender?.ikn || tender?.id,
|
||||||
announcement: tender?.aciklama || '',
|
announcement: tender?.aciklama || '',
|
||||||
deadline: [tender?.tarih || '', tender?.saat || ''],
|
deadline: [tender?.tarih || '', tender?.saat || ''],
|
||||||
|
status: tender?.durum || '',
|
||||||
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
|
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
|
||||||
procurementTerms: results?.procurementTerms || null,
|
procurementTerms: results?.procurementTerms || null,
|
||||||
analysis: preparationAnalysisSource(results),
|
analysis: preparationAnalysisSource(results),
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
export const tenderStateKey = t => String(t?.ikn || t?.id || '').trim();
|
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 const USER_STATE_FIELDS = ['takipte', 'incelendi', 'kanbanStatus', 'atananPersonel', 'gorev', 'notes', 'checklist', 'bidDraft'];
|
||||||
export function applyTenderStates(tenders, records = {}) {
|
export function applyTenderStates(tenders, records = {}) {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const result = tenders.map(t => {
|
const result = tenders.map(t => {
|
||||||
const key = tenderStateKey(t); seen.add(key);
|
const key = tenderStateKey(t); seen.add(key);
|
||||||
const record = records[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)) {
|
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))
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
export function tenderSnapshot(t) {
|
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) {
|
export function publicTender(tender) {
|
||||||
|
|||||||
@@ -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,[]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user