Persist observed tender source revisions and expose paginated history

This commit is contained in:
2026-09-06 10:49:30 +03:00
parent a0f4824c7e
commit 68fe84e9c7
9 changed files with 166 additions and 0 deletions
+2
View File
@@ -1,3 +1,4 @@
import { TenderSourceHistory } from './TenderSourceHistory';
import React, { useState, useEffect, useRef, useId } from 'react';
import { useApp } from '../context/AppContext';
import { BidPackageBuilder } from './BidPackageBuilder';
@@ -105,6 +106,7 @@ 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.ikn && <TenderSourceHistory key={`${authUser?.email}:${tender.ikn}:${tender.kurum}`} tender={tender} />}
{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>
+40
View File
@@ -0,0 +1,40 @@
import React, {useEffect,useRef,useState} from 'react';
import {getToken} from '../services/auth';
const labels={baslik:'Başlık',kurum:'İdare',tarih:'Tarih',saat:'Saat',durum:'Durum',alimTuru:'Alım türü',aciklama:'İlan metni',documentVersion:'Doküman sürümü'};
export function TenderSourceHistory({tender}) {
const [state,setState]=useState({records:[],nextCursor:null,loaded:false,error:null,loading:false});
const active=useRef(null);
useEffect(()=>()=>{const pending=active.current;active.current=null;pending?.abort();},[]);
const load=async(before=null)=>{
if(active.current)return;
const controller=new AbortController();active.current=controller;
const token=getToken(),timer=setTimeout(()=>controller.abort(),30000);
setState(s=>({...s,loading:true,error:null}));
try {
const query=new URLSearchParams({ikn:tender.ikn,kurum:tender.kurum || ''});if(before)query.set('before',before);
const res=await fetch('/api/tenders/source-history?'+query,{headers:{Authorization:`Bearer ${token}`},signal:controller.signal});
const data=await res.json();
if(!res.ok || !data.success || !Array.isArray(data.records))throw new Error(data.error || 'Kaynak geçmişi alınamadı.');
if(!controller.signal.aborted && getToken()===token)setState(s=>({records:before?[...s.records,...data.records]:data.records,nextCursor:data.nextCursor,loaded:true,error:null,loading:false}));
} catch(error) {
if(active.current===controller && getToken()===token)setState(s=>({...s,loading:false,error:controller.signal.aborted?'Kaynak geçmişi zamanında alınamadı. Yeniden deneyin.':error.message}));
} finally {clearTimeout(timer);if(active.current===controller)active.current=null;}
};
return <details className="p-3 border-b text-sm">
<summary className="cursor-pointer font-semibold">Kaynak değişiklik geçmişi</summary>
<p className="mt-2 text-xs text-slate-600">Uygulamanın gözlemlediği sürümler gösterilir. Gözlem zamanı resmi değişiklik tarihi değildir; kayıt öncesindeki veya iki kontrol arasındaki değişiklikler bulunmayabilir. Farklı kaynaklar ayrı izlenir.</p>
{!state.loaded && <button onClick={()=>load()} disabled={state.loading} className="mt-2 px-3 py-2 border rounded">Geçmişi yükle</button>}
{state.loading && <p role="status">Kaynak geçmişi yükleniyor…</p>}
{state.error && <div role="alert"><p>{state.error}</p><button onClick={()=>load(state.loaded?state.nextCursor:null)} disabled={state.loading} className="underline py-2">Yeniden dene</button></div>}
{state.loaded && !state.records.length && <p className="mt-2">Henüz gözlem kaydı yok. Bu, ihale bilgilerinin hiç değişmediğini göstermez.</p>}
<ol className="space-y-3 mt-3">{state.records.map(record=><li key={record.id} className="border rounded p-2">
<p>{new Date(record.observedAt).toLocaleString('tr-TR')} · {record.sourceId}</p>
{!record.changes.length?<p>Bu kaynak için ilk gözlem.</p>:record.changes.map(change=><details key={change.field} className="mt-1">
<summary className="cursor-pointer">{labels[change.field] || change.field} değişti</summary>
<p className="whitespace-pre-wrap break-words">Önce: {change.before ?? 'Bilinmiyor'}</p>
<p className="whitespace-pre-wrap break-words">Sonra: {change.after ?? 'Bilinmiyor'}</p>
</details>)}
</li>)}</ol>
{state.nextCursor && <button onClick={()=>load(state.nextCursor)} disabled={state.loading} className="mt-2 px-3 py-2 border rounded">Daha eski kayıtlar</button>}
</details>;
}
+1
View File
@@ -268,6 +268,7 @@ export const AppProvider = ({ children }) => {
const data = await fetchLiveTenders({ refresh });
setTenders(prev => applyTenderStates(mergeUserState(data.tenders, prev), savedTenderStates.current));
setLiveDataSource({ source: data.source, fetchedAt: data.fetchedAt });
if(data.historyWarning)setLiveDataError(data.historyWarning);
setLiveStats({
toplamIhale: data.totalCount ?? 0,
bugunEklenen: data.todayCount ?? 0,