import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'; import { getToken } from '../services/auth'; import { listDocuments, downloadDocument } 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 [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || 'birim'); const kaynakGecerlilik = /(?:geçerlilik[^.]{0,100}?|ihale tarihinden itibaren\s*)(\d+)\s*(?:takvim\s*)?gün/i.exec(tender?.aciklama || ''); const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik?.[1] || ''); const [kisim, setKisim] = useState(storedDraft.kisim || ''); const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || ''); const effectiveResults = results || storedDraft.analysisSource || {}; 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:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,sourceReview:effectiveResults.sourceReview || null}}}); 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 = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || ''); const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1])); const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)}); const analizEksik = effectiveResults.sourceReview?.status === 'partial'; const hazir = 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 { 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ü', '
Belgeler kontrol ediliyor…
'); setPrinting(true); const currentFiles = await listDocuments(); 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)}; setVault(currentVault); const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault); if (fileErrors.length) throw new Error(fileErrors.join(' ')); const f = formVerisi(); if (tip === 'excel') { const buffer = await createBidWorkbook(f,satirlar); if (!mounted.current || session !== getToken() || submitted !== latestDraft.current) throw new Error('Form veya oturum değişti; Excel çıktısını yeniden oluşturun.'); 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 ({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.
} {eskiFiyatlar &&Eski taslak fiyatları satır sırasına göre kaydedilmiş. Yanlış ürüne aktarılmaması için fiyatları yeniden girin.
}Analiz kısmi: kaynak veya kalem listesi eksik. Tam dokümanı ve resmi cetveli kontrol ederek analizi tamamlayın.
} {/* Sizden beklenen bilgiler */} {eksikler.length > 0 && (Sizden beklenen bilgiler
Belgeleri hazırlayabilmek için şunlar gerekli:{' '} {eksikler.map((a) => a.l).join(', ')}. Bir kez girdiğinizde bu tarayıcıda saklanır, sonraki ihalelerde tekrar sorulmaz.
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.
{message}
)} >}Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.
| Kalem | Miktar | Birim fiyat | Tutar |
|---|---|---|---|
| {r.name || '—'} | {miktar ?? '—'} {birim} | 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" /> | {tutar ? paraTR(tutar) : '—'} |
{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.'}
)}Yazı ile: {yaziylaTL(toplam)}
)}{hata}
{kalemEksik && 'Seçilen her kalemde ad, miktar, birim ve fiyat gerekli. '} {turCeliskisi && 'İlan birim fiyat teklif belirtiyor; götürü bedel seçilemez. '} {sureEksik && 'Teklif geçerlilik süresini kaynak dokümana göre doğrulayın. '}
} {/* Belgeler */}{eksikler.length > 0 ? 'Yukarıdaki zorunlu alanları doldurun.' : 'Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.'}
)}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.