Guide bid preparation errors to editable fields
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user