Files
ihaleist/src/components/BidPackageBuilder.jsx
T

513 lines
32 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { preparationAnalysisSource, preparationConditionSections, conditionText } from '../services/preparationAnalysis';
import { preparationDeadlineProblem } from '../services/preparationDeadline';
import { bidValidityForForm, bidTypeForForm } from '../services/bidTerms';
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 { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices, bidSourceOptions } from '../services/bidSource';
import { rowLot, selectBidLots } from '../services/bidLots';
import { PreparationChecks } from './PreparationChecks';
import { preparationBlockers, documentKey } from '../services/preparationChecks';
import { useApp } from '../context/AppContext';
import {
FolderCheck, ChevronDown, ChevronUp, FileSignature, Table2, ListChecks,
AlertTriangle, Printer, Info,
} from 'lucide-react';
import {
yaziylaTL, paraTR, miktarAyristir, belgeYazdir,
teklifMektubuHtml, teklifCetveliHtml, evrakListesiHtml,
} from '../services/bidDocs';
/**
* İhale Dosyası Hazırla — derin analiz bittikten sonra teklif zarfına girecek
* belgeleri üretir. Analizden gelen kalemler fiyatlandırılır, eksik firma
* bilgileri kullanıcıya sorulur; belgeler tam Türkçe olarak yazdırılır.
*
* 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 }) => {
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
const storedDraft = tender?.bidDraft || {};
const [vault,setVault] = useState({loading:true,error:null,documents:[]});
const [vaultRefresh,setVaultRefresh] = 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;};
},[authUser?.email,vaultRefresh]);
const [dirty, setDirty] = useState(false);
const [acik, setAcik] = useState(true);
const [hata, setHata] = useState(null);
const [firma, setFirma] = useState(() => {
let kayitli = {};
try { kayitli = JSON.parse(localStorage.getItem(FIRMA_ANAHTAR + ':' + authUser?.email) || '{}'); } catch { /* yoksay */ }
return {
...bosFirma(),
firmaUnvan: userProfile?.company || '',
...kayitli,
...(storedDraft.firma || {}),
};
});
const effectiveResults = results || storedDraft.analysisSource || {};
const kaynakTeklifTuru = bidTypeForForm(tender?.aciklama,effectiveResults.procurementTerms?.bidType);
const [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || kaynakTeklifTuru.type || '');
const kaynakGecerlilik = bidValidityForForm(tender?.aciklama, effectiveResults.procurementTerms?.bidValidity);
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik.days || '');
const [kisim, setKisim] = useState(storedDraft.kisim || '');
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
const [itemSource, setItemSource] = useState(storedDraft.itemSource || {});
const sourceOptions = bidSourceOptions(tender, effectiveResults, itemSource);
const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []);
const [checks, setChecks] = useState(storedDraft.checks || {});
const documents = [...new Map([...(effectiveResults.documentChecklist || []), ...manualDocuments].map(d => [documentKey(d),d])).values()];
const [partialBid,setPartialBid] = useState(storedDraft.partialBid || {enabled:false,source:'',selected:[],assignments:{}});
const kalemler = sourceRows(tender, effectiveResults, sourceOptions.selected || 'tender');
const lotSelection = selectBidLots(kalemler,partialBid);
const allLots = [...new Set(kalemler.map(r=>rowLot(r,partialBid.assignments)).filter(Boolean))];
const sourceSnapshot = JSON.stringify([bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler), partialBid, itemSource, sourceOptions.signature]);
const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
const eskiFiyatlar = storedDraft.priceSchema !== 1 && Object.keys(storedDraft.fiyatlar || {}).length > 0;
const alanYaz = (k, v) => {
const yeni = { ...firma, [k]: v };
setFirma(yeni);
try { localStorage.setItem(FIRMA_ANAHTAR + ':' + authUser?.email, JSON.stringify(yeni)); } catch { /* kota */ }
};
const satirlar = useMemo(
() => lotSelection.rows.map(k => ({ ...k, birimFiyat: fiyatlar[k.priceKey] })),
[kalemler, fiyatlar, partialBid],
);
const kalemToplami = useMemo(() => roundMoney(satirlar.reduce((t,r) => t + (bidRowTotal(r) ?? 0),0)), [satirlar]);
const latestDraft = useRef('');
latestDraft.current = JSON.stringify([firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource]);
const mounted = useRef(true);
const printWindow = useRef(null);
const [printing,setPrinting] = useState(false);
useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]);
const saveDraft = async () => {
const submitted = latestDraft.current;
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:preparationAnalysisSource(effectiveResults)}});
if (!mounted.current) return;
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
};
const toplam = teklifTuru === 'goturu' ? (parseBidPrice(goturuBedel) || 0) : kalemToplami;
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
const turCeliskisi = !['birim','goturu'].includes(teklifTuru) || kaynakTeklifTuru.status === 'conflict' || (kaynakTeklifTuru.type && teklifTuru !== kaynakTeklifTuru.type);
const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days);
const [deadlineNow,setDeadlineNow] = useState(() => new Date());
useEffect(() => {const timer=setInterval(()=>setDeadlineNow(new Date()),30000);return ()=>clearInterval(timer);},[]);
const deadlineProblem = preparationDeadlineProblem(tender,deadlineNow);
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn});
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 formVerisi = () => ({
...firma,
teklifTuru,
gecerlilikGun,
kisim: partialBid.enabled ? partialBid.selected.join(', ') : kisim,
idare: tender?.kurum || '',
isinAdi: tender?.baslik || '',
ikn: tender?.ikn || '',
ihaleTarihi: tender?.ihaleTarihi || tender?.tarih || '',
});
const uret = async (tip) => {
if (printWindow.current) return;
const submitted = latestDraft.current;
const session = getToken();
setHata(null);
try {
const initialDeadlineProblem=preparationDeadlineProblem(tender);
if(initialDeadlineProblem)throw new Error(initialDeadlineProblem);
if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.');
// Open during the click; async validation must not lose popup activation.
printWindow.current = belgeYazdir('Dosya kontrolü', '<p>Belgeler kontrol ediliyor…</p>');
setPrinting(true);
const currentFiles = await listDocuments();
const reviewedText = effectiveResults.sourceReview?.humanReview;
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form, kaynak veya oturum değişti. Güncel bilgilerle yeniden çıktı oluşturun.');
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender),tenderIkn:tender.ikn};
setVault(currentVault);
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
if (fileErrors.length) throw new Error(fileErrors.join(' '));
const currentDeadlineProblem=preparationDeadlineProblem(tender);
if(currentDeadlineProblem)throw new Error(currentDeadlineProblem);
const f = formVerisi();
if (tip === 'excel') {
const buffer = await createBidWorkbook(f,satirlar);
if (reviewedText) await validateDocumentTextReview(reviewedText,tender.ikn);
if (!mounted.current || session !== getToken() || submitted !== latestDraft.current)
throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.');
const finalDeadlineProblem=preparationDeadlineProblem(tender);
if(finalDeadlineProblem)throw new Error(finalDeadlineProblem);
downloadBidWorkbook(buffer,tender.ikn);
printWindow.current?.close();
}
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam), printWindow.current);
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam), printWindow.current);
if (tip === 'liste') belgeYazdir('Evrak Kontrol Listesi', evrakListesiHtml(f, documents), printWindow.current);
printWindow.current = null;
} catch (e) {
printWindow.current?.close();
printWindow.current = null;
if(mounted.current) setHata(e.message);
} finally { if(mounted.current) setPrinting(false); }
};
return (
<div className="bg-white rounded-2xl border-2 border-emerald-300 shadow-xs overflow-hidden">
<button
type="button"
onClick={() => setAcik((o) => !o)}
className="w-full flex items-center gap-3 p-5 text-left hover:bg-emerald-50/50 transition-colors"
>
<div className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shrink-0">
<FolderCheck className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-black text-sm text-slate-900">İhale Dosyası Hazırla</h4>
<p className="text-[11px] text-slate-500 leading-relaxed">
Analizdeki kalemleri fiyatlandırın; teklif mektubu, birim fiyat teklif cetveli ve
evrak kontrol listesi hazır olsun.
</p>
</div>
{acik ? <ChevronUp className="w-5 h-5 text-slate-400 shrink-0" />
: <ChevronDown className="w-5 h-5 text-slate-400 shrink-0" />}
</button>
{acik && (
<div className="border-t border-emerald-200 p-5 space-y-5" onChange={() => setDirty(true)}>
<div className="flex items-center gap-3">
<button type="button" onClick={saveDraft} disabled={savingCount > 0}
className="px-4 py-2 rounded-lg bg-blue-700 text-white text-xs disabled:opacity-50">Hazırlık taslağını kaydet</button>
<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">
{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.
</p>}
{eskiFiyatlar && <p className="text-xs text-amber-900">Eski taslak fiyatları satır sırasına göre kaydedilmiş. Yanlış ürüne aktarılmaması için fiyatları yeniden girin.</p>}
<label className="flex items-start gap-2 text-xs text-slate-800">
<input type="checkbox" checked={!kaynakKontrolGerekli}
onChange={e => setReviewedSource(e.target.checked ? sourceSnapshot : null)} />
Bu sürümün kalemlerini, miktarlarını, teklif türünü, geçerlilik süresini ve aşağıdaki yeterlik/risk koşullarını resmi dokümanla karşılaştırdım.
</label>
</div>
{preparationConditionSections.some(([field]) => effectiveResults[field]?.length > 0) &&
<details className="rounded-xl border border-amber-300 p-3" open>
<summary className="font-bold text-sm cursor-pointer">Yeterlik, risk ve diğer kaynak koşulları</summary>
<p className="text-xs mt-2">Analiz bulgularını resmi maddelerle karşılaştırın. Bu liste bütün koşulların bulunduğunu veya karşılandığını kanıtlamaz.</p>
{preparationConditionSections.filter(([field]) => effectiveResults[field]?.length > 0).map(([field,label]) =>
<section key={field} className="mt-3 text-xs"><h5 className="font-bold">{label}</h5>
<ul className="list-disc pl-5 space-y-1">{effectiveResults[field].map((row,index)=><li key={index}>{conditionText(row)}</li>)}</ul>
</section>)}
</details>}
{sourceOptions.conflict && <fieldset className="rounded-xl border border-amber-400 bg-amber-50 p-3 space-y-2">
<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 => {
setItemSource({source:e.target.value,signature:sourceOptions.signature});
setReviewedSource(null); setDirty(true);
}} className="block mt-1 rounded border p-2 w-full">
<option value="">Resmi cetveli kontrol ederek seçin</option>
<option value="tender">İhale havuzu — {sourceOptions.tenderCount} kalem</option>
<option value="analysis">Doküman analizi — {sourceOptions.analysisCount} kalem</option>
</select>
</label>
{sourceOptions.needsSelection && <p role="alert" className="text-xs text-red-700">Kaynak seçilmeden çıktı hazırlanamaz. Kaynak listeleri değiştiğinde seçim yeniden yapılmalıdır.</p>}
</fieldset>}
{kaynakTeklifTuru.evidence.length > 0 && <div className="text-xs border rounded-lg p-3 space-y-1">
<p className="font-bold">Teklif türünün kaynağı</p>
{kaynakTeklifTuru.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı teklif türleri var. Güncel resmi dokümanı ve karma kapsamı kontrol edin.</p>}
{[...new Set(kaynakTeklifTuru.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
</div>}
{kaynakGecerlilik.evidence.length > 0 && <div className="text-xs border rounded-lg p-3 space-y-1">
<p className="font-bold">Teklif geçerlilik süresinin kaynağı</p>
{kaynakGecerlilik.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı süreler var. Güncel resmi dokümanı belirleyip analizi yenileyin; çelişki çözülmeden çıktı alınamaz.</p>}
{[...new Set(kaynakGecerlilik.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
</div>}
{effectiveResults.sourceReview?.humanReview && <p className="text-xs text-slate-600">
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>}
{/* Sizden beklenen bilgiler */}
{eksikler.length > 0 && (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-3.5 flex gap-2.5">
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
<div className="min-w-0">
<p className="text-xs font-bold text-amber-900">Sizden beklenen bilgiler</p>
<p className="text-[11px] text-amber-800 leading-relaxed mt-0.5">
Belgeleri hazırlayabilmek için şunlar gerekli:{' '}
<strong>{eksikler.map((a) => a.l).join(', ')}</strong>.
Bir kez girdiğinizde bu tarayıcıda saklanır, sonraki ihalelerde tekrar sorulmaz.
</p>
</div>
</div>
)}
{/* Firma bilgileri */}
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">İstekli (firma) bilgileri</h5>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{ALANLAR.map((a) => {
const eksik = a.zorunlu && !String(firma[a.k] || '').trim();
return (
<label key={a.k} className={a.genis ? 'sm:col-span-2' : ''}>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
{a.l}{a.zorunlu && <span className="text-red-600"> *</span>}
</span>
<input
value={firma[a.k] || ''}
onChange={(e) => alanYaz(a.k, e.target.value)}
placeholder={a.ph || ''}
className={`w-full text-xs px-3 py-2 rounded-lg border bg-white focus:ring-2 focus:ring-emerald-500 outline-none ${
eksik ? 'border-amber-400 bg-amber-50/40' : 'border-slate-300'
}`}
/>
</label>
);
})}
</div>
</div>
{/* Teklif ayarları */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif türü</span>
<select
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"
>
<option value="">Resmi dokümana göre seçin</option>
<option value="birim">Birim fiyat</option>
<option value="goturu">Anahtar teslim götürü bedel</option>
</select>
</label>
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Teklif geçerlilik (gün)</span>
<input
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"
/>
</label>
<label>
<span className="text-[11px] font-semibold text-slate-600 block mb-1">Kısım (varsa)</span>
<input
value={kisim}
onChange={(e) => setKisim(e.target.value)}
placeholder="Örn. 2. kısım"
className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300"
/>
</label>
</div>
<section 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 && <>
<label className="block text-xs">Kısmi teklif izni — resmi doküman ve madde
<input className="w-full border rounded p-2" value={partialBid.source || ''} onChange={e=>setPartialBid(p=>({...p,source:e.target.value}))}/>
</label>
<p className="text-xs text-slate-600">Resmi kısım numaralarını cetvelden doğrulayın. Aynı kısımdaki tüm kalemler birlikte seçilir; seçilmeyenler toplam ve çıktı dışında kalır.</p>
<div className="flex flex-wrap gap-3">{allLots.map(lot=><label key={lot} className="text-xs flex gap-1">
<input type="checkbox" checked={(partialBid.selected || []).includes(lot)} onChange={e=>setPartialBid(p=>({...p,selected:e.target.checked ? [...(p.selected || []),lot] : (p.selected || []).filter(l=>l!==lot)}))}/>
Kısım {lot}
</label>)}</div>
<div className="max-h-64 overflow-auto space-y-2">{kalemler.map(row=><label key={row.priceKey} className="flex items-center gap-2 text-xs">
<span className="flex-1">{row.name}</span><span>Resmi kısım</span>
<input aria-label={`${row.name} resmi kısım`} className="border rounded p-1 w-24" value={rowLot(row,partialBid.assignments)} readOnly={row.kisim != null || row.lot != null}
onChange={e=>setPartialBid(p=>({...p,assignments:{...p.assignments,[row.priceKey]:e.target.value}}))}/>
</label>)}</div>
{lotSelection.errors.map(message=><p key={message} role="alert" className="text-xs text-red-700">{message}</p>)}
</>}
</section>
{/* Fiyatlandırma */}
{teklifTuru === 'goturu' ? (
<label className="block">
<span className="text-[11px] font-semibold text-slate-600 block mb-1">
Teklif edilen toplam bedel (KDV hariç, TL)
</span>
<input
value={goturuBedel}
onChange={(e) => setGoturuBedel(e.target.value)}
placeholder="0"
className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300"
/>
</label>
) : lotSelection.rows.length > 0 ? (
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Kalem fiyatlandırma</h5>
<p className="text-xs text-slate-500">Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.</p>
<div className="overflow-x-auto">
<table className="w-full text-xs min-w-[520px]">
<thead>
<tr className="text-[11px] text-slate-500 border-b border-slate-200">
<th className="text-left py-1.5 font-semibold">Kalem</th>
<th className="text-right py-1.5 font-semibold w-24">Miktar</th>
<th className="text-right py-1.5 font-semibold w-32">Birim fiyat</th>
<th className="text-right py-1.5 font-semibold w-28">Tutar</th>
</tr>
</thead>
<tbody>
{satirlar.map((r, i) => {
const { miktar, birim } = miktarAyristir(r.quantity);
const tutar = bidRowTotal(r);
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 text-right text-slate-600 whitespace-nowrap">
{miktar ?? '—'} {birim}
</td>
<td className="py-1.5 text-right">
<input
value={fiyatlar[r.priceKey] ?? ''}
onChange={(e) =>
setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value }))}
placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'}
className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300"
/>
</td>
<td className="py-1.5 text-right font-semibold text-slate-900 whitespace-nowrap">
{tutar ? paraTR(tutar) : '—'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{kalemler.some((k) => k.estimatedUnitPrice != null) && (
<p className="text-[10px] text-slate-400 flex items-start gap-1.5">
<Info className="w-3 h-3 shrink-0 mt-0.5" />
Gri yazılı değerler şartnamede geçen tahmini birim fiyatlardır — teklifiniz değildir.
</p>
)}
</div>
) : (
<p className="text-xs text-slate-500 bg-slate-50 border border-slate-200 rounded-lg p-3">
{kalemler.length ? 'Henüz bir kısım seçilmedi. Yukarıdaki resmi kısım eşleştirmesini ve seçimini tamamlayın.' : 'Kalem dökümü bulunamadı. Resmi teklif cetvelini İhale Dosyaları bölümünden yükleyin. Kalem eksikliği teklif türünü değiştirmez.'}
</p>
)}
<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">
<summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary>
<ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul>
</details>}
{/* 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">
<span className="text-xs font-semibold text-slate-300">Teklif toplamı (KDV hariç)</span>
<span className="text-lg font-black text-amber-300">{paraTR(toplam)} TL</span>
</div>
{toplam > 0 && (
<p className="text-[11px] text-slate-400 leading-relaxed">Yazı ile: {yaziylaTL(toplam)}</p>
)}
</div>
{hata && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex gap-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800 leading-relaxed">{hata}</p>
</div>
)}
{(kalemEksik || turCeliskisi || sureEksik) && <p role="alert" className="text-xs bg-amber-50 p-3 text-amber-900">
{kalemEksik && 'Seçilen her kalemde ad, miktar, birim ve fiyat gerekli. '}
{turCeliskisi && 'Teklif türünü resmi dokümana göre seçin; kaynakla farklılık varsa giderin. '}
{sureEksik && 'Teklif geçerlilik süresini kaynak dokümana göre doğrulayın. '}
</p>}
{/* Belgeler */}
<div className="space-y-2">
<h5 className="font-extrabold text-xs text-slate-900">Hazırlanacak belgeler</h5>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
{[
{ t: 'mektup', ad: 'Teklif Mektubu', ikon: FileSignature, aciklama: 'Rakam ve yazı ile bedel' },
{ t: 'cetvel', ad: 'Birim Fiyat Teklif Cetveli', ikon: Table2, aciklama: `${lotSelection.rows.length} kalem` },
{ t: 'excel', ad: 'Excel Teklif Cetveli', ikon: Table2, aciklama: `${lotSelection.rows.length} kalem · XLSX` },
{ t: 'liste', ad: 'Evrak Kontrol Listesi', ikon: ListChecks, aciklama: `${documents.length} belge` },
].map((b) => (
<button
key={b.t}
type="button"
disabled={!hazir || printing || ((b.t === 'excel' || b.t === 'cetvel') && teklifTuru !== 'birim')}
onClick={() => uret(b.t)}
className="text-left bg-white border border-slate-300 hover:border-emerald-500 hover:bg-emerald-50/50 disabled:opacity-40 disabled:hover:border-slate-300 disabled:hover:bg-white rounded-xl p-3 transition-colors"
>
<b.ikon className="w-4 h-4 text-emerald-600 mb-1.5" />
<span className="block text-xs font-bold text-slate-900 leading-snug">{b.ad}</span>
<span className="block text-[10px] text-slate-500 mt-0.5">{b.aciklama}</span>
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-700 mt-1.5">
<Printer className="w-3 h-3" /> {b.t === 'excel' ? 'Excel indir' : 'Yazdır / PDF'}
</span>
</button>
))}
</div>
{!hazir && (
<p className="text-[11px] text-slate-500">
{eksikler.length > 0
? 'Yukarıdaki zorunlu alanları doldurun.'
: 'Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.'}
</p>
)}
</div>
<p className="text-[10px] text-slate-400 leading-relaxed">
Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce
ihale dokümanındaki form metinleriyle karşılaştırın.
</p>
</div>
)}
</div>
);
};