Guide bid preparation errors to editable fields

This commit is contained in:
2026-09-06 13:15:22 +03:00
parent 7994df1cdd
commit 6fd6ac04f4
4 changed files with 89 additions and 9 deletions
+61 -6
View File
@@ -5,7 +5,7 @@ import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'
import { getToken } from '../services/auth';
import { listDocuments, downloadDocument, validateDocumentTextReview } from '../services/account';
import { tenderDateKey } from '../services/tenderCalendar';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import React, { useState, useMemo, useEffect, useRef, useId } from 'react';
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices, bidSourceOptions } from '../services/bidSource';
import { rowLot, selectBidLots } from '../services/bidLots';
@@ -58,6 +58,15 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
.catch(error=>{if(!cancelled)setVault({loading:false,error:error.message,documents:[]});});
return ()=>{cancelled=true;};
},[authUser?.email,vaultRefresh]);
const fieldPrefix = useId();
const preparationTargets = useRef({});
const targetRef = key => node => { preparationTargets.current[key] = node; };
const goToMissing = key => {
const target = preparationTargets.current[key];
if (!target) return;
target.scrollIntoView({block:'center'});
target.focus({preventScroll:true});
};
const [dirty, setDirty] = useState(false);
const [acik, setAcik] = useState(true);
const [hata, setHata] = useState(null);
@@ -136,6 +145,22 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
const hazir = !deadlineProblem && eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
const missingActions = [
...(deadlineProblem ? [{target:'source',text:deadlineProblem}] : []),
...(kaynakKontrolGerekli ? [{target:'source',text:'Resmi dokümanla kaynak karşılaştırmasını tamamlayın.'}] : []),
...(analizEksik ? [{target:'analysis',text:'Kısmi analizi tam ve doğrulanabilir kaynakla tamamlayın.'}] : []),
...(sourceOptions.needsSelection ? [{target:'itemSource',text:'Resmi cetvele uygun kalem kaynağını seçin.'}] : []),
...eksikler.map(a=>({target:`firm:${a.k}`,text:`${a.l} bilgisini doldurun.`})),
...(turCeliskisi ? [{target:'bidType',text:'Teklif türünü kaynak dokümana göre doğrulayın.'}] : []),
...(sureEksik ? [{target:'validity',text:'Teklif geçerlilik süresini doğrulayın.'}] : []),
...lotSelection.errors.map(text=>({target:'lots',text})),
...(teklifTuru === 'birim' ? (satirlar.length ? satirlar.flatMap((row,i)=>
validateBidRows([row]).length ? [{target:`price:${row.priceKey}`,text:`${i+1}. kalem (${row.name || 'Adı eksik'}): ad, miktar, birim ve fiyatı kontrol edin.`}] : [])
: [{target:'pricing',text:'Resmi teklif cetvelindeki kalemleri yükleyin veya kısım seçimini tamamlayın.'}]) : []),
...(teklifTuru === 'goturu' && !(toplam > 0) ? [{target:'lumpSum',text:'Geçerli, sıfırdan büyük toplam teklif bedeli girin.'}] : []),
...kontrolEksikleri.map(text=>({target:'checks',text})),
];
const formVerisi = () => ({
...firma,
teklifTuru,
@@ -221,7 +246,17 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<span role="status" className="text-xs">{dirty ? 'Kaydedilmemiş değişiklikler var' : storedDraft.firma ? 'Sunucuda kayıtlı taslak' : 'Henüz kaydedilmedi'}</span>
</div>
<div className="rounded-xl border border-amber-300 bg-amber-50 p-3 space-y-2">
{missingActions.length > 0 && <section aria-label="Hazırlık eksikleri" className="rounded-xl border border-amber-300 bg-amber-50 p-3">
<h5 className="text-sm font-bold">Çıktı öncesi tamamlanacaklar</h5>
<p className="text-xs mt-1">İlgili alana gitmek için eksikliğe tıklayın. Taslağı eksikleriyle kaydedebilirsiniz.</p>
<ul className="text-xs mt-2 space-y-2 max-h-64 overflow-auto">
{missingActions.map((item,i)=><li key={`${item.target}:${i}`}>
<button type="button" onClick={()=>goToMissing(item.target)} className="text-left text-blue-800 underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-700">{item.text}</button>
</li>)}
</ul>
</section>}
<div ref={targetRef('source')} tabIndex={-1} className="rounded-xl border border-amber-300 bg-amber-50 p-3 space-y-2">
{kaynakKontrolGerekli && <p role="alert" className="text-xs text-amber-900">
{reviewedSource ? 'İlan, kalem veya belge bilgisi değişti. Önceki kontrol bu sürüm için geçerli değil.' : 'Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı.'}
{' '}Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz.
@@ -248,7 +283,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<legend className="text-sm font-bold">Kalem kaynakları farklı</legend>
<p className="text-xs">İhale havuzundaki liste ile analiz listesi aynı değil. Resmi cetvelle karşılaştırıp kullanılacak listeyi seçin. Seçim uygunluk onayı değildir; ardından kalem, fiyat ve belge kontrollerini tamamlayın.</p>
<label className="block text-xs">Teklifte kullanılacak kalem listesi
<select value={sourceOptions.selected} onChange={e => {
<select ref={targetRef('itemSource')} value={sourceOptions.selected} onChange={e => {
setItemSource({source:e.target.value,signature:sourceOptions.signature});
setReviewedSource(null); setDirty(true);
}} className="block mt-1 rounded border p-2 w-full">
@@ -273,7 +308,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
Metin incelemesi: sürüm {effectiveResults.sourceReview.humanReview.revision} · {effectiveResults.sourceReview.humanReview.reviewedBy} · {effectiveResults.sourceReview.humanReview.reviewedAt}. Kullanıcı karşılaştırmasıdır; resmi onay değildir.
</p>}
{deadlineProblem && <p role="alert" className="text-xs text-red-700">{deadlineProblem}</p>}
{analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.</p>}
{analizEksik && <p ref={targetRef('analysis')} tabIndex={-1} role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak, OCR veya kalem bilgisi kontrol gerektiriyor. {(effectiveResults.sourceReview.issues || []).join(' ')} Tam ve doğrulanabilir kaynakla analizi tamamlayın.</p>}
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-3.5 flex gap-2.5">
@@ -301,6 +336,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{a.l}{a.zorunlu && <span className="text-red-600"> *</span>}
</span>
<input
ref={targetRef(`firm:${a.k}`)}
aria-invalid={eksik || undefined}
value={firma[a.k] || ''}
onChange={(e) => alanYaz(a.k, e.target.value)}
placeholder={a.ph || ''}
@@ -319,6 +356,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif türü</span>
<select
ref={targetRef('bidType')}
value={teklifTuru}
onChange={(e) => setTeklifTuru(e.target.value)}
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white"
@@ -331,6 +369,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif geçerlilik (gün)</span>
<input
ref={targetRef('validity')}
value={gecerlilikGun}
onChange={(e) => setGecerlilikGun(e.target.value.replace(/\D/g, ''))}
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300"
@@ -347,7 +386,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</label>
</div>
<section className="border rounded-xl p-3 space-y-2" aria-label="Kısmi teklif seçimi">
<section ref={targetRef('lots')} tabIndex={-1} className="border rounded-xl p-3 space-y-2" aria-label="Kısmi teklif seçimi">
<label className="flex gap-2 text-xs font-bold"><input type="checkbox" checked={partialBid.enabled}
onChange={e=>setPartialBid(p=>({...p,enabled:e.target.checked}))}/>Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum</label>
{partialBid.enabled && <>
@@ -368,6 +407,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</>}
</section>
<section ref={targetRef('pricing')} tabIndex={-1} aria-label="Teklif fiyatlandırma">
{/* Fiyatlandırma */}
{teklifTuru === 'goturu' ? (
<label className="block">
@@ -375,6 +415,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
Teklif edilen toplam bedel (KDV hariç, TL)
</span>
<input
ref={targetRef('lumpSum')}
value={goturuBedel}
onChange={(e) => setGoturuBedel(e.target.value)}
placeholder="0"
@@ -399,14 +440,24 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const tutar = bidRowTotal(r);
const invalidRow = validateBidRows([r]).length > 0;
const errorId = `${fieldPrefix}-price-error-${i}`;
return (
<tr key={r.priceKey} className="border-b border-slate-100">
<td className="py-1.5 pr-2 text-slate-800">{r.name || '—'}</td>
<td className="py-1.5 pr-2 text-slate-800">{r.name || '—'}
{invalidRow && <p id={errorId} className="text-red-700 mt-1">{!(miktar > 0) || !birim || !r.name?.trim()
? 'Kalem adı, miktarı ve birimi için resmi cetveli kontrol edin; kaynağı düzeltip analizi yenileyin.'
: 'Geçerli, sıfırdan büyük birim fiyat girin.'}</p>}
</td>
<td className="py-1.5 text-right text-slate-600 whitespace-nowrap">
{miktar ?? '—'} {birim}
</td>
<td className="py-1.5 text-right">
<input
ref={targetRef(`price:${r.priceKey}`)}
aria-label={`${i+1}. kalem ${r.name || 'Adı eksik'} birim fiyat (TL)`}
aria-invalid={invalidRow || undefined}
aria-describedby={invalidRow ? errorId : undefined}
value={fiyatlar[r.priceKey] ?? ''}
onChange={(e) =>
setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value }))}
@@ -436,6 +487,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
</p>
)}
</section>
<section ref={targetRef('checks')} tabIndex={-1} aria-label="Belge ve ürün uygunluğu kontrolleri">
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={{...vault,tenderIkn:tender.ikn}} refreshVault={()=>setVaultRefresh(n=>n+1)} downloadFile={(file)=>downloadDocument(file.id,file.ad).catch(e=>setHata(e.message))}/>
{kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3">
@@ -443,6 +496,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
<ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul>
</details>}
</section>
{/* Toplam */}
<div className="bg-slate-900 text-white rounded-xl p-4 space-y-1">
<div className="flex items-baseline justify-between gap-3 flex-wrap">
+3 -3
View File
@@ -221,7 +221,7 @@ export const TenderDetailModal = () => {
<div className="flex items-center gap-2 border-b border-slate-200 overflow-x-auto pb-1">
{[
{ id: 'details', label: 'İhale Detayları', icon: FileText },
{ id: 'pdf_stamp', label: 'Birim Fiyat Cetveli (PDF & Kaşe)', icon: Stamp, badge: 'İnteraktif' },
{ id: 'pdf_stamp', label: 'Teklif Dosyası Hazırla', icon: Stamp, badge: 'İnteraktif' },
{ id: 'ai_assistant', label: 'Yapay Zeka Asistanı', icon: Sparkles, badge: tender?.aiMatchScore != null ? `%${tender.aiMatchScore} Uyum` : undefined },
{ id: 'checklist', label: 'Hazırlık Checklist', icon: CheckSquare },
{ id: 'teklif_asistani', label: 'Teklif Asistanı', icon: PenLine, badge: 'AI' },
@@ -416,7 +416,7 @@ export const TenderDetailModal = () => {
onClick={() => setActiveTab('pdf_stamp')}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2.5 px-3 rounded-lg text-xs flex items-center justify-center gap-2 shadow-2xs"
>
<Download className="w-4 h-4" /> Birim Fiyat Cetveli (PDF Aç/Düzenle)
<Download className="w-4 h-4" /> Teklif Dosyası Hazırla (PDF / Excel)
</button>
<button
onClick={handleExportICS}
@@ -432,7 +432,7 @@ export const TenderDetailModal = () => {
</div>
)}
{/* Tab 2: PDF Birim Fiyat Cetveli & Kaşe Editörü */}
{/* Tab 2: Teklif dosyası hazırlığı */}
{activeTab === 'pdf_stamp' && (
<BidPackageBuilder key={tender.ikn || tender.id} tender={tender}
results={aiAnalysisResults?.tenderKey === (tender.ikn || tender.id) ? aiAnalysisResults : null} />