diff --git a/docs/quality/qa-preparation-overview.json b/docs/quality/qa-preparation-overview.json new file mode 100644 index 0000000..2c28550 --- /dev/null +++ b/docs/quality/qa-preparation-overview.json @@ -0,0 +1,18 @@ +{ + "date": "2026-09-06", + "environment": "localhost:5182 five synthetic tenders, real UI/API save", + "checks": { + "fiveDossiers": true, + "allUnassignedVisible": true, + "directFieldFocus": true, + "savedFieldCleared": true, + "ownerUpdated": true, + "reloadPreserved": true + }, + "limits": [ + "Synthetic incomplete dossiers, not five official bid approvals", + "One direct field focus, saved edit and one owner assignment tested", + "Distinct owners and document revision errors covered by unit tests", + "Real PDF/official documents remain open" + ] +} diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 053f593..b60feac 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -874,3 +874,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Kayıt sırasında durum/atama/sürükleme kilitlenir; mevcut sunucu kayıt hatası panoda gösterilir. Durum iyimser olarak değiştirilmez, mevcut mutateTender kalıcılık akışı korunur. - Yerel 5182 sentetik Giresun görevinde 5 kontrol geçti: sahte düğmenin kalkması, desteklenmeme açıklaması, durum seçimi, yenileme sonrası durumun korunması ve erişilebilir kontrol adları. Kanıt qa-kanban-controls.json. Derleme geçti. Önceki 197 test tekrar çalıştırılmadı; sunucu değişmedi. - Bu tur atama mutasyonu, hata enjeksiyonu, yalnız klavyeyle tam gezinme veya beş dosyalı eksik/sorumlu/sonraki işlem özetinin kabulünü kanıtlamaz. D07'nin ortak hazırlık özeti açık. + +### D07 — Beş dosyalı hazırlık özeti — 6 Eylül 2026 +- Kanban'a Hazırlık dosyaları özeti eklendi. Aynı takip/sorumlu filtresindeki tüm ihaleler için sorumlu, kayıtlı taslaktaki kontrol sayısı, belge eksikleri ve sıradaki kontrol bir tabloda gösterilir. Kaydedilmemiş form değişiklikleri özetin dışında olduğu açıklanır; temiz özet başvuru onayı olarak sunulmaz. +- preparationSummary mevcut kaynak/kalem/kısım, firma alanları, teklif türü/geçerliliği, fiyat, sağlık koşulları ve belge bağlantı/sürüm/süre kontrollerini kullanır. Zorunlu firma alanları ortak modüle taşındı. Belge deposu hata/yükleme durumları gösterilir; yenileme düğmesi vardır. +- Özet bağlantıları detayın doğrudan hazırlık sekmesini açar ve ilgili firma/fiyat/kontrol alanına odak taşır. Belge ve ürün eksikleri ilgili kontrol bölümüne gider. Çıktı öncesi canlı belge doğrulamaları korunur. +- 200 otomatik test ve derleme geçti. Beş ayrı sorumlu/belge içeren taslak özeti, eski belge sürümü/depo hatası ve geçmiş deadline önceliği test edildi. +- 5182 yerel tarayıcıda beş sentetik görev tek tabloda görüldü; vergi alanına doğrudan odak, kaydedilen düzeltmenin özetten kalkması, sorumlu atamasının yansıması ve yenileme sonrası kalıcılık dahil 6 kontrol geçti (qa-preparation-overview.json). +- Bu sentetik kabul gerçek beş resmi dosyanın uygunluğunu kanıtlamaz. Firma portföyü/ürün kapasitesi, tam mobil erişilebilirlik, her eksik belgeye ayrı alt-alan odağı ve resmi çıktılarla ilgili açık maddeler devam eder. Özet kontrolleri kayıtlı taslağa dayanır; yeni AI analizi veya kaydedilmemiş değişiklikler dosya ekranında ayrıca değerlendirilir. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index 9e72e0a..dc70924 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,3 +1,4 @@ +import { bidFirmFields as ALANLAR } from '../services/bidFirmFields'; import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis'; import { preparationDeadlineProblem } from '../services/preparationDeadline'; import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms'; @@ -29,23 +30,13 @@ import { * Firma bilgileri localStorage'da tutulur — her ihalede yeniden sorulmasın. */ -const ALANLAR = [ - { k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true }, - { k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true }, - { k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true }, - { k: 'vergiNo', l: 'Vergi numarası', zorunlu: true }, - { k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false }, - { k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true }, - { k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' }, - { k: 'telefon', l: 'Telefon', zorunlu: true }, - { k: 'eposta', l: 'E-posta', zorunlu: true }, -]; + const FIRMA_ANAHTAR = 'ihaleist_firma_bilgi'; const bosFirma = () => Object.fromEntries(ALANLAR.map((a) => [a.k, ''])); -export const BidPackageBuilder = ({ results, tender: selectedTender }) => { +export const BidPackageBuilder = ({ results, tender: selectedTender, initialFocus }) => { const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp(); const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)}; const storedDraft = tender?.bidDraft || {}; @@ -67,6 +58,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { target.scrollIntoView({block:'center'}); target.focus({preventScroll:true}); }; + useEffect(()=>{ + if(!initialFocus)return; + const frame=requestAnimationFrame(()=>goToMissing(initialFocus)); + return ()=>cancelAnimationFrame(frame); + },[initialFocus]); const [dirty, setDirty] = useState(false); const [acik, setAcik] = useState(true); const [hata, setHata] = useState(null); diff --git a/src/components/KanbanBoard.jsx b/src/components/KanbanBoard.jsx index 533cea1..3620e20 100644 --- a/src/components/KanbanBoard.jsx +++ b/src/components/KanbanBoard.jsx @@ -1,3 +1,4 @@ +import { PreparationOverview } from './PreparationOverview'; import { useDeadlineClock } from '../hooks/useDeadlineClock'; import { deadlineInfo } from '../services/tenderCalendar'; import React, { useState } from 'react'; @@ -91,6 +92,7 @@ export const KanbanBoard = () => { {savingCount > 0 &&

Değişiklik kaydediliyor…

} {saveError &&

{saveError}

} + {/* Kanban Columns Grid */}
{columns.map(col => { diff --git a/src/components/PreparationOverview.jsx b/src/components/PreparationOverview.jsx new file mode 100644 index 0000000..0b08538 --- /dev/null +++ b/src/components/PreparationOverview.jsx @@ -0,0 +1,46 @@ +import React,{useEffect,useState} from 'react'; +import {listDocuments} from '../services/account'; +import {preparationSummary} from '../services/preparationSummary'; +import {useApp} from '../context/AppContext'; +import {getToken} from '../services/auth'; + +export function PreparationOverview({tenders,now}) { + const {authUser}=useApp(); + return ; +} +function Overview({tenders,now}) { + const {setActiveTenderDetail}=useApp(); + const [vault,setVault]=useState({loading:true,error:null,documents:[]}),[refresh,setRefresh]=useState(0); + useEffect(()=>{ + let cancelled=false; + setVault({loading:true,error:null,documents:[]}); + listDocuments().then(data=>{if(!cancelled)setVault({loading:false,error:null,documents:data.belgeler});}) + .catch(error=>{if(!cancelled)setVault({loading:false,error:error.message,documents:[]});}); + return()=>{cancelled=true;}; + },[refresh]); + const open=(tender,target)=>setActiveTenderDetail({...tender,preparationTarget:target}); + return
+

Hazırlık dosyaları · {tenders.length}

+

Panodaki ve sorumlu filtresindeki kayıtlı taslaklar gösterilir. Kaydetmediğiniz değişiklikler bu özete girmez. Eksik sayısı başvuru onayı değildir; çıktı sırasında belgeler yeniden kontrol edilir.

+ + {vault.loading&&

Belge durumu kontrol ediliyor…

} + {vault.error&&

Belge durumu doğrulanamadı: {vault.error}

} + {!tenders.length ?

İhaleyi takibe alın veya hazırlık görevi açın; dosyası burada görünsün.

: +
+ + {tenders.map(tender=>{ + const summary=preparationSummary(tender,vault,now); + return + + + + + ; + })} +
İhaleSorumluBelge eksikleriSıradaki kontrol

{summary.issues.length ? `${summary.issues.length} kontrol eksik` : 'Kayıtlı kontroller tamam; son kontrol gerekli'}

{summary.owner}
{summary.documentIssues.length ? `${summary.documentIssues.length} belge kontrolü` : 'Kayıtlı belgelerde eksik bulunmadı'} +
    {summary.documentIssues.map((text,i)=>
  • )}
+
+
Tüm eksikleri göster
    {summary.issues.map((item,i)=>
  • )}
+
} +
; +} diff --git a/src/components/TenderDetailModal.jsx b/src/components/TenderDetailModal.jsx index 663a923..8a20d1f 100644 --- a/src/components/TenderDetailModal.jsx +++ b/src/components/TenderDetailModal.jsx @@ -42,6 +42,7 @@ export const TenderDetailModal = () => { } }; const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders' + useEffect(()=>{setActiveTab(activeTenderDetail?.preparationTarget ? 'pdf_stamp' : 'details');},[activeTenderDetail?.id,activeTenderDetail?.preparationTarget]); const noteOwner = authUser?.email || ''; const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} })); // Discard private drafts on account changes, before rendering another account. @@ -434,7 +435,7 @@ export const TenderDetailModal = () => { {/* Tab 2: Teklif dosyası hazırlığı */} {activeTab === 'pdf_stamp' && ( - )} diff --git a/src/services/bidFirmFields.js b/src/services/bidFirmFields.js new file mode 100644 index 0000000..d96c7d8 --- /dev/null +++ b/src/services/bidFirmFields.js @@ -0,0 +1,11 @@ +export const bidFirmFields = [ + { k: 'firmaUnvan', l: 'Firma ticaret unvanı', zorunlu: true, genis: true }, + { k: 'adres', l: 'Tebligata esas adres', zorunlu: true, genis: true }, + { k: 'vergiDairesi', l: 'Vergi dairesi', zorunlu: true }, + { k: 'vergiNo', l: 'Vergi numarası', zorunlu: true }, + { k: 'sicilNo', l: 'Ticaret sicil / MERSİS no', zorunlu: false }, + { k: 'yetkiliAd', l: 'Yetkili ad soyad', zorunlu: true }, + { k: 'yetkiliGorev', l: 'Yetkilinin görevi', zorunlu: true, ph: 'Örn. Genel Müdür' }, + { k: 'telefon', l: 'Telefon', zorunlu: true }, + { k: 'eposta', l: 'E-posta', zorunlu: true }, +]; diff --git a/src/services/preparationSummary.js b/src/services/preparationSummary.js new file mode 100644 index 0000000..b2be417 --- /dev/null +++ b/src/services/preparationSummary.js @@ -0,0 +1,41 @@ +import {bidFirmFields} from './bidFirmFields.js'; +import {sourceRows,bidSourceOptions,bidSourceSnapshot,restoreBidPrices} from './bidSource.js'; +import {selectBidLots} from './bidLots.js'; +import {bidTotal,bidAmount,validateBidRows} from './bidValidation.js'; +import {bidTypeForForm,bidValidityForForm} from './bidTerms.js'; +import {documentKey,preparationBlockers} from './preparationChecks.js'; +import {preparationDeadlineProblem} from './preparationDeadline.js'; +import {tenderDateKey} from './tenderCalendar.js'; + +// Saved draft diagnostics only. A clean summary still requires live output checks. +export function preparationSummary(tender,vault,now=new Date()) { + const draft=tender.bidDraft || {},analysis=draft.analysisSource || {},issues=[]; + const add=(target,text)=>issues.push({target,text}); + const deadline=preparationDeadlineProblem(tender,now); + if(deadline)add('source',deadline); + const documents=[...new Map([...(analysis.documentChecklist || []),...(draft.manualDocuments || [])].map(d=>[documentKey(d),d])).values()]; + const itemSource=draft.itemSource || {},partial=draft.partialBid || {enabled:false,source:'',selected:[],assignments:{}}; + const options=bidSourceOptions(tender,analysis,itemSource),rows=sourceRows(tender,analysis,options.selected || 'tender'); + const selection=selectBidLots(rows,partial); + const snapshot=JSON.stringify([bidSourceSnapshot(tender,{...analysis,documentChecklist:documents},rows),partial,itemSource,options.signature]); + if(draft.reviewedSource!==snapshot)add('source','Resmi kaynak karşılaştırması gerekli.'); + if(analysis.sourceReview?.status==='partial')add('analysis','Analiz kısmi; kaynak kontrolünü tamamlayın.'); + if(options.needsSelection)add('itemSource','Resmi cetvele uygun kalem listesini seçin.'); + for(const field of bidFirmFields)if(field.zorunlu&&!String(draft.firma?.[field.k] || '').trim())add(`firm:${field.k}`,`${field.l} kayıtlı taslakta eksik.`); + const type=bidTypeForForm(tender.aciklama,analysis.procurementTerms?.bidType),bidType=draft.teklifTuru || type.type || ''; + if(!['birim','goturu'].includes(bidType)||type.status==='conflict'||(type.type&&type.type!==bidType))add('bidType','Teklif türünü doğrulayın.'); + const validity=bidValidityForForm(tender.aciklama,analysis.procurementTerms?.bidValidity),days=Number(draft.gecerlilikGun || validity.days || ''); + if(!Number.isInteger(days)||days<=0||validity.status==='conflict'||(validity.days&&daysadd('lots',text)); + const prices=restoreBidPrices(draft),priced=selection.rows.map(row=>({...row,birimFiyat:prices[row.priceKey]})); + if(bidType==='birim') { + if(!priced.length)add('pricing','Resmi teklif kalemleri eksik.'); + priced.forEach((row,i)=>{if(validateBidRows([row]).length)add(`price:${row.priceKey}`,`${i+1}. kalemin miktar, birim veya fiyatını tamamlayın.`);}); + if(priced.length&&!validateBidRows(priced).length&&bidTotal(priced)===null)add('pricing','Genel toplam geçersiz veya hesaplama sınırını aşıyor.'); + } + if(bidType==='goturu'&&bidAmount(draft.goturuBedel)===null)add('lumpSum','Toplam teklif bedelini kontrol edin.'); + const checkedVault={...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn}; + const documentIssues=preparationBlockers([],documents,draft.checks,checkedVault); + preparationBlockers(selection.rows,documents,draft.checks,checkedVault).forEach(text=>add('checks',text)); + return {issues,documentIssues,owner:tender.atananPersonel || 'Atanmadı',next:issues[0] || {target:'source',text:'Dosyayı açıp son kontrolleri yapın.'}}; +} diff --git a/tests/preparation-summary.test.mjs b/tests/preparation-summary.test.mjs new file mode 100644 index 0000000..9840584 --- /dev/null +++ b/tests/preparation-summary.test.mjs @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {preparationSummary} from '../src/services/preparationSummary.js'; +import {documentKey,documentRevision} from '../src/services/preparationChecks.js'; +const now=new Date('2026-09-06T10:00:00Z'); +const tender={id:'test',ikn:'2026/1',tarih:'10.09.2026',saat:'10:00',baslik:'Lab',items:[{ad:'Kit',miktar:2,birim:'adet'}]}; +const vault={loading:false,error:null,documents:[]}; +test('five saved dossiers retain distinct owners, missing documents and actionable targets',()=>{ + const rows=Array.from({length:5},(_,i)=>({...tender,id:`t-${i}`,ikn:`2026/${i}`,atananPersonel:`Sorumlu ${i}`,bidDraft:{manualDocuments:[{document:`Belge ${i}`}],teklifTuru:'birim'}})); + rows.forEach((row,i)=>{ + const result=preparationSummary(row,vault,now); + assert.equal(result.owner,`Sorumlu ${i}`); + assert.ok(result.documentIssues.some(text=>text.includes(`Belge ${i}`))); + assert.equal(result.next.target,'source'); + assert.ok(result.issues.some(issue=>issue.target.startsWith('price:'))); + assert.ok(result.issues.some(issue=>issue.target==='firm:vergiNo')); + }); +}); +test('document revision and vault failures invalidate a previously verified saved document',()=>{ + const doc={document:'Ürün belgesi'}; + const file={id:'f',tur:'firma',olusturma:'2026-09-01',boyut:20,son_kullanma:'2026-12-31'}; + const draft={manualDocuments:[doc],checks:{documents:{[documentKey(doc)]:{status:'verified',evidence:'Madde 2',documentId:'f',documentRevision:documentRevision(file)}}}}; + assert.deepEqual(preparationSummary({...tender,bidDraft:draft},{...vault,documents:[file]},now).documentIssues,[]); + assert.ok(preparationSummary({...tender,bidDraft:draft},{...vault,documents:[{...file,boyut:21}]},now).documentIssues.some(text=>text.includes('değişti'))); + assert.ok(preparationSummary({...tender,bidDraft:draft},{...vault,error:'offline'},now).documentIssues.some(text=>text.includes('doğrulanamadı'))); +}); +test('expired source takes priority over completion or manually moved Kanban status',()=>{ + const summary=preparationSummary({...tender,tarih:'01.09.2026',kanbanStatus:'teklif_verildi'},vault,now); + assert.match(summary.next.text,/zamanı geçti/); + assert.equal(summary.next.target,'source'); + assert.equal(summary.owner,'Atanmadı'); +});