fix: share persisted preparation checks across analysis screens

This commit is contained in:
2026-09-06 08:44:10 +03:00
parent e4b02f5dd3
commit 05c7e7ecdc
4 changed files with 52 additions and 43 deletions
+8 -1
View File
@@ -40,7 +40,14 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- Kalem bazında ürün/model veya hizmet kapsamı, teknik uygunluk kaynağı, sorumlu, ÜTS, numune/demonstrasyon, servis/kurulum ve teslimat kontrolleri bulunur. Uygulanmıyor seçimi gerekçe ister; uygun olmayan ürün çıktıyı engeller. - Kalem bazında ürün/model veya hizmet kapsamı, teknik uygunluk kaynağı, sorumlu, ÜTS, numune/demonstrasyon, servis/kurulum ve teslimat kontrolleri bulunur. Uygulanmıyor seçimi gerekçe ister; uygun olmayan ürün çıktıyı engeller.
- Eksik kontrollerin listesi fiyatlandırma ekranında görünür. Bu alanlar kullanıcı beyanı/kontrol kaydıdır; belge deposundaki gerçek dosyaya otomatik bağlantı ve resmi yeterlik doğrulaması henüz yoktur. - Eksik kontrollerin listesi fiyatlandırma ekranında görünür. Bu alanlar kullanıcı beyanı/kontrol kaydıdır; belge deposundaki gerçek dosyaya otomatik bağlantı ve resmi yeterlik doğrulaması henüz yoktur.
- Kontroller, elle eklenen belgeler ve analizden gelen kalem/belge listesi taslakla saklanır. HTTP testi bu alanların hesap içinde yazılıp geri okunmasını doğrular. Üç kural testi eksik kaynak, uygun olmayan ürün, zorunlu belgenin atlanması ve değişmiş gereksinimleri kapsar. - Kontroller, elle eklenen belgeler ve analizden gelen kalem/belge listesi taslakla saklanır. HTTP testi bu alanların hesap içinde yazılıp geri okunmasını doğrular. Üç kural testi eksik kaynak, uygun olmayan ürün, zorunlu belgenin atlanması ve değişmiş gereksinimleri kapsar.
- Tarayıcıda tüm bu alanları doldurma, yenileme ve çok kalemli kullanılabilirlik kabulü bekliyor. AI raporundaki eski yerel checklist işaretleri henüz bu kalıcı kontrol modeline bağlanmadı. D06/D07 tamamlanmış sayılmaz. - Tarayıcıda tüm bu alanları doldurma, yenileme ve çok kalemli kullanılabilirlik kabulü bekliyor. AI raporundaki eski yerel belge işaretleri kaldırıldı; rapor kalıcı hazırlık kaydını özetler ve kontrol formuna yönlendirir. D06/D07 tamamlanmış sayılmaz.
## Ekranlar arası kayıt tutarlılığı
- Derin rapor belge listesi artık geçici tamamlandı işaretleri tutmaz; kayıtlı kullanıcı kontrollerini gösterir ve kaynaklı hazırlık formuna geçiş sağlar.
- Yol haritası tamamlanma durumu ihale checklist kaydında sunucu onayıyla saklanır. Eylem veya kaynak değişince eski işaret tamamlanmış sayılmaz. Kaydetme hatası görünürdür.
- Derin rapor ve hazırlık formu, açılışta seçilmiş eski nesne yerine ihale havuzundaki güncel kullanıcı kaydını okur. Böylece AI penceresinden tekrar açılan formda yeni kaydedilmiş taslağa erişilir.
- HTTP testinde yol haritası kaynak/durum alanlarının yazma-okuma kontrolü eklendi. Arayüzde iki pencere arasında geçiş ve yeniden giriş senaryosu hâlâ ayrıca doğrulanmalıdır.
## Doğrulama ## Doğrulama
+3 -2
View File
@@ -37,8 +37,9 @@ const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, ''])); const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
export const BidPackageBuilder = ({ results, tender }) => { export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const { userProfile, mutateTender, savingCount, authUser } = useApp(); const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
const storedDraft = tender?.bidDraft || {}; const storedDraft = tender?.bidDraft || {};
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [acik, setAcik] = useState(true); const [acik, setAcik] = useState(true);
+37 -37
View File
@@ -1,8 +1,11 @@
import React, { useState } from 'react'; import React, { useRef } from 'react';
import { useApp } from '../context/AppContext';
import { bidSourceSnapshot } from '../services/bidSource';
import { documentKey } from '../services/preparationChecks';
import { YeterlilikRozeti } from './YeterlilikRozeti'; import { YeterlilikRozeti } from './YeterlilikRozeti';
import { import {
AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route, AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route,
HelpCircle, Coins, Target, CheckSquare, Square, Package, HelpCircle, Coins, Target, Package,
} from 'lucide-react'; } from 'lucide-react';
import { BidPackageBuilder } from './BidPackageBuilder'; import { BidPackageBuilder } from './BidPackageBuilder';
@@ -45,8 +48,16 @@ const Baslik = ({ icon: Icon, renk, children, not }) => (
</h4> </h4>
); );
export const DeepAnalysisReport = ({ results, tender }) => { export const DeepAnalysisReport = ({ results, tender: selectedTender }) => {
const [isaretli, setIsaretli] = useState({}); const {tenders, mutateTender, savingCount, saveError} = useApp();
const preparationRef = useRef(null);
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
const sourceSnapshot = bidSourceSnapshot(tender, results);
const savedSteps = Array.isArray(tender.checklist) ? tender.checklist : [];
const stepDone = key => savedSteps.some(s => s.key === key && s.sourceSnapshot === sourceSnapshot && s.done);
const toggleStep = key => mutateTender(tender.id, {checklist:[
...savedSteps.filter(s => s.key !== key), {key, sourceSnapshot, done:!stepDone(key)},
]});
if (!results) return null; if (!results) return null;
const { const {
@@ -59,7 +70,6 @@ export const DeepAnalysisReport = ({ results, tender }) => {
dolu(roadmap) || dolu(questionsForAdministration) || dolu(itemsAndSpecs); dolu(roadmap) || dolu(questionsForAdministration) || dolu(itemsAndSpecs);
if (!varMi) return null; if (!varMi) return null;
const degistir = (k) => setIsaretli((o) => ({ ...o, [k]: !o[k] }));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -181,41 +191,26 @@ export const DeepAnalysisReport = ({ results, tender }) => {
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Belge kontrol listesi */} {/* Source requirements summarize the single saved preparation checklist. */}
{dolu(documentChecklist) && ( {dolu(documentChecklist) && (
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3"> <div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<Baslik icon={ClipboardList} renk="text-blue-600" not="işaretleyerek ilerleyin"> <Baslik icon={ClipboardList} renk="text-blue-600">Belge Gereksinimleri</Baslik>
Belge Kontrol Listesi <p className="text-xs text-slate-500">Kontrolleri aşağıdaki hazırlık formunda kaynaklarıyla kaydedin.</p>
</Baslik>
<ul className="space-y-1.5"> <ul className="space-y-1.5">
{documentChecklist.map((b, i) => { {documentChecklist.map((b, i) => {
const k = `b${i}`; const check = tender.bidDraft?.checks?.documents?.[documentKey(b)];
const secili = !!isaretli[k]; return <li key={i} className="bg-slate-50 border border-slate-200 rounded-lg p-2.5 space-y-1">
return ( <span className="text-xs font-semibold">{b.document || '—'}</span>
<li key={i}> <p className="text-[11px] text-slate-500">{b.note}</p>
<button
type="button"
onClick={() => degistir(k)}
className="w-full text-left flex items-start gap-2 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-lg p-2.5 transition-colors"
>
{secili
? <CheckSquare className="w-4 h-4 text-emerald-600 shrink-0 mt-0.5" />
: <Square className="w-4 h-4 text-slate-300 shrink-0 mt-0.5" />}
<span className="flex-1 min-w-0">
<span className={`text-xs font-semibold block ${secili ? 'line-through text-slate-400' : 'text-slate-800'}`}>
{b.document || '—'}
</span>
{b.note && <span className="text-[11px] text-slate-500 leading-snug block mt-0.5">{b.note}</span>}
</span>
<YeterlilikRozeti belge={b.document} /> <YeterlilikRozeti belge={b.document} />
{b.mandatory === false <p className="text-[11px] text-slate-600">{check?.status === 'verified' && check?.evidence?.trim()
? <span className="text-[10px] font-bold text-slate-500 bg-slate-200 px-1.5 py-0.5 rounded shrink-0">İsteğe bağlı</span> ? 'Kayıtlı kullanıcı kontrolü var; güncel kaynak kontrolünü hazırlık formundan doğrulayın.'
: <span className="text-[10px] font-bold text-red-700 bg-red-100 px-1.5 py-0.5 rounded shrink-0">Zorunlu</span>} : 'Belge kontrolü bekliyor.'}</p>
</button> </li>;
</li>
);
})} })}
</ul> </ul>
<button type="button" onClick={() => {preparationRef.current?.scrollIntoView({behavior:'smooth',block:'start'});preparationRef.current?.focus({preventScroll:true});}}
className="text-xs font-bold text-blue-700">Belge ve ürün kontrollerine git</button>
</div> </div>
)} )}
@@ -262,15 +257,17 @@ export const DeepAnalysisReport = ({ results, tender }) => {
</Baslik> </Baslik>
<ol className="space-y-2"> <ol className="space-y-2">
{roadmap.map((a, i) => { {roadmap.map((a, i) => {
const k = `y${i}`; const k = JSON.stringify(a);
const secili = !!isaretli[k]; const secili = stepDone(k);
return ( return (
<li key={i} className="flex gap-3"> <li key={i} className="flex gap-3">
<div className="flex flex-col items-center shrink-0"> <div className="flex flex-col items-center shrink-0">
<button <button
type="button" type="button"
onClick={() => degistir(k)} disabled={savingCount > 0}
aria-label={`${i + 1}. adımı tamamlandı işaretle`} onClick={() => toggleStep(k)}
aria-label={`${i + 1}. adımı ${secili ? 'yeniden aç' : 'tamamlandı işaretle'}`}
aria-pressed={secili}
className={`w-7 h-7 rounded-full text-white text-[11px] font-black flex items-center justify-center transition-colors ${ className={`w-7 h-7 rounded-full text-white text-[11px] font-black flex items-center justify-center transition-colors ${
secili ? 'bg-emerald-600' : (FAZ_RENK[a.phase] || 'bg-slate-700') secili ? 'bg-emerald-600' : (FAZ_RENK[a.phase] || 'bg-slate-700')
}`} }`}
@@ -323,7 +320,10 @@ export const DeepAnalysisReport = ({ results, tender }) => {
)} )}
{/* Analiz ve yol haritası okunduktan sonraki adım: teklif zarfını hazırla */} {/* Analiz ve yol haritası okunduktan sonraki adım: teklif zarfını hazırla */}
{saveError && <p role="alert" className="text-xs text-red-700">{saveError}</p>}
<div ref={preparationRef} tabIndex={-1} aria-label="Kalıcı ihale hazırlığı">
<BidPackageBuilder key={tender?.ikn || tender?.id} results={results} tender={tender} /> <BidPackageBuilder key={tender?.ikn || tender?.id} results={results} tender={tender} />
</div>
</div> </div>
); );
+2 -1
View File
@@ -27,9 +27,10 @@ after(async()=>{if(child){child.kill();await new Promise(r=>child.once('exit',r)
test('HTTP preparation requires auth, persists draft and rejects stale revisions',async()=>{ test('HTTP preparation requires auth, persists draft and rejects stale revisions',async()=>{
assert.equal((await request('/api/me/tender-state','GET',null,null)).status,401); assert.equal((await request('/api/me/tender-state','GET',null,null)).status,401);
const draft={firma:{firmaUnvan:'QA Laboratuvar'},priceSchema:1,pricesBySource:{kit1:'12.50'},gecerlilikGun:'120',checks:{products:{kit1:{product:'Test kit',status:'suitable',source:'Teknik 3.2'}},documents:{doc1:{status:'verified',evidence:'Test v1'}}},manualDocuments:[{document:'Yetki',note:'7.1',mandatory:true}],analysisSource:{itemsAndSpecs:[{name:'Kit',quantity:'2 adet'}],documentChecklist:[]}}; const draft={firma:{firmaUnvan:'QA Laboratuvar'},priceSchema:1,pricesBySource:{kit1:'12.50'},gecerlilikGun:'120',checks:{products:{kit1:{product:'Test kit',status:'suitable',source:'Teknik 3.2'}},documents:{doc1:{status:'verified',evidence:'Test v1'}}},manualDocuments:[{document:'Yetki',note:'7.1',mandatory:true}],analysisSource:{itemsAndSpecs:[{name:'Kit',quantity:'2 adet'}],documentChecklist:[]}};
const data={key:'2026/1582991',patch:{takipte:true,gorev:{kontrolListesi:[]},bidDraft:draft},snapshot:{id:'ilan-qa',ikn:'2026/1582991'},version:0}; const data={key:'2026/1582991',patch:{takipte:true,gorev:{kontrolListesi:[]},bidDraft:draft,checklist:[{key:'collect-documents',sourceSnapshot:'source-v1',done:true}]},snapshot:{id:'ilan-qa',ikn:'2026/1582991'},version:0};
assert.equal((await request('/api/me/tender-state','PUT',data)).status,200); assert.equal((await request('/api/me/tender-state','PUT',data)).status,200);
const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft); const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft);
assert.deepEqual(read.data.records[data.key].data.checklist,data.patch.checklist);
assert.equal((await request('/api/me/tender-state','PUT',data)).status,409); assert.equal((await request('/api/me/tender-state','PUT',data)).status,409);
assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{}); assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{});
}); });