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.
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
<YeterlilikRozeti belge={b.document} />
|
||||||
type="button"
|
<p className="text-[11px] text-slate-600">{check?.status === 'verified' && check?.evidence?.trim()
|
||||||
onClick={() => degistir(k)}
|
? 'Kayıtlı kullanıcı kontrolü var; güncel kaynak kontrolünü hazırlık formundan doğrulayın.'
|
||||||
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"
|
: 'Belge kontrolü bekliyor.'}</p>
|
||||||
>
|
</li>;
|
||||||
{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} />
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</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 */}
|
||||||
<BidPackageBuilder key={tender?.ikn || tender?.id} results={results} tender={tender} />
|
{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>
|
</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()=>{
|
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,{});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user