Persist observed tender source revisions and expose paginated history
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user