fix: share persisted preparation checks across analysis screens
This commit is contained in:
@@ -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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi';
|
||||
|
||||
const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, '']));
|
||||
|
||||
export const BidPackageBuilder = ({ results, tender }) => {
|
||||
const { userProfile, mutateTender, savingCount, authUser } = useApp();
|
||||
export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
|
||||
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
|
||||
const storedDraft = tender?.bidDraft || {};
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [acik, setAcik] = useState(true);
|
||||
|
||||
@@ -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 {
|
||||
AlertOctagon, AlertTriangle, Eye, CalendarClock, ClipboardList, Route,
|
||||
HelpCircle, Coins, Target, CheckSquare, Square, Package,
|
||||
HelpCircle, Coins, Target, Package,
|
||||
} from 'lucide-react';
|
||||
import { BidPackageBuilder } from './BidPackageBuilder';
|
||||
|
||||
@@ -45,8 +48,16 @@ const Baslik = ({ icon: Icon, renk, children, not }) => (
|
||||
</h4>
|
||||
);
|
||||
|
||||
export const DeepAnalysisReport = ({ results, tender }) => {
|
||||
const [isaretli, setIsaretli] = useState({});
|
||||
export const DeepAnalysisReport = ({ results, tender: selectedTender }) => {
|
||||
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;
|
||||
|
||||
const {
|
||||
@@ -59,7 +70,6 @@ export const DeepAnalysisReport = ({ results, tender }) => {
|
||||
dolu(roadmap) || dolu(questionsForAdministration) || dolu(itemsAndSpecs);
|
||||
if (!varMi) return null;
|
||||
|
||||
const degistir = (k) => setIsaretli((o) => ({ ...o, [k]: !o[k] }));
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Belge kontrol listesi */}
|
||||
{/* Source requirements summarize the single saved preparation checklist. */}
|
||||
{dolu(documentChecklist) && (
|
||||
<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">
|
||||
Belge Kontrol Listesi
|
||||
</Baslik>
|
||||
<Baslik icon={ClipboardList} renk="text-blue-600">Belge Gereksinimleri</Baslik>
|
||||
<p className="text-xs text-slate-500">Kontrolleri aşağıdaki hazırlık formunda kaynaklarıyla kaydedin.</p>
|
||||
<ul className="space-y-1.5">
|
||||
{documentChecklist.map((b, i) => {
|
||||
const k = `b${i}`;
|
||||
const secili = !!isaretli[k];
|
||||
return (
|
||||
<li key={i}>
|
||||
<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>
|
||||
const check = tender.bidDraft?.checks?.documents?.[documentKey(b)];
|
||||
return <li key={i} className="bg-slate-50 border border-slate-200 rounded-lg p-2.5 space-y-1">
|
||||
<span className="text-xs font-semibold">{b.document || '—'}</span>
|
||||
<p className="text-[11px] text-slate-500">{b.note}</p>
|
||||
<YeterlilikRozeti belge={b.document} />
|
||||
{b.mandatory === false
|
||||
? <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>
|
||||
: <span className="text-[10px] font-bold text-red-700 bg-red-100 px-1.5 py-0.5 rounded shrink-0">Zorunlu</span>}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
<p className="text-[11px] text-slate-600">{check?.status === 'verified' && check?.evidence?.trim()
|
||||
? 'Kayıtlı kullanıcı kontrolü var; güncel kaynak kontrolünü hazırlık formundan doğrulayın.'
|
||||
: 'Belge kontrolü bekliyor.'}</p>
|
||||
</li>;
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -262,15 +257,17 @@ export const DeepAnalysisReport = ({ results, tender }) => {
|
||||
</Baslik>
|
||||
<ol className="space-y-2">
|
||||
{roadmap.map((a, i) => {
|
||||
const k = `y${i}`;
|
||||
const secili = !!isaretli[k];
|
||||
const k = JSON.stringify(a);
|
||||
const secili = stepDone(k);
|
||||
return (
|
||||
<li key={i} className="flex gap-3">
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => degistir(k)}
|
||||
aria-label={`${i + 1}. adımı tamamlandı işaretle`}
|
||||
disabled={savingCount > 0}
|
||||
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 ${
|
||||
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 */}
|
||||
{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} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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()=>{
|
||||
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 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);
|
||||
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.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user