@@ -5,7 +5,7 @@ import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook'
import { getToken } from '../services/auth' ;
import { getToken } from '../services/auth' ;
import { listDocuments , downloadDocument , validateDocumentTextReview } from '../services/account' ;
import { listDocuments , downloadDocument , validateDocumentTextReview } from '../services/account' ;
import { tenderDateKey } from '../services/tenderCalendar' ;
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 { bidRowTotal , validateBidRows , roundMoney , parseBidPrice } from '../services/bidValidation' ;
import { sourceRows , bidSourceSnapshot , restoreBidPrices , bidSourceOptions } from '../services/bidSource' ;
import { sourceRows , bidSourceSnapshot , restoreBidPrices , bidSourceOptions } from '../services/bidSource' ;
import { rowLot , selectBidLots } from '../services/bidLots' ;
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 : [ ] } ) ; } ) ;
. catch ( error => { if ( ! cancelled ) setVault ( { loading : false , error : error . message , documents : [ ] } ) ; } ) ;
return ( ) => { cancelled = true ; } ;
return ( ) => { cancelled = true ; } ;
} , [ authUser ? . email , vaultRefresh ] ) ;
} , [ 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 [ dirty , setDirty ] = useState ( false ) ;
const [ acik , setAcik ] = useState ( true ) ;
const [ acik , setAcik ] = useState ( true ) ;
const [ hata , setHata ] = useState ( null ) ;
const [ hata , setHata ] = useState ( null ) ;
@@ -136,6 +145,22 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const analizEksik = effectiveResults . sourceReview ? . status === 'partial' ;
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 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 = ( ) => ( {
const formVerisi = ( ) => ( {
... firma ,
... firma ,
teklifTuru ,
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 >
< span role = "status" className = "text-xs" > { dirty ? 'Kaydedilmemiş değişiklikler var' : storedDraft . firma ? 'Sunucuda kayı tlı taslak' : 'Henüz kaydedilmedi' } < / span >
< / div >
< / 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" >
{ 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ı .' }
{ 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 .
{ ' ' } 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 >
< 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 >
< 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
< 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 } ) ;
setItemSource ( { source : e . target . value , signature : sourceOptions . signature } ) ;
setReviewedSource ( null ) ; setDirty ( true ) ;
setReviewedSource ( null ) ; setDirty ( true ) ;
} } className = "block mt-1 rounded border p-2 w-full" >
} } 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 .
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 > }
< / p > }
{ deadlineProblem && < p role = "alert" className = "text-xs text-red-700" > { deadlineProblem } < / 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 */ }
{ /* Sizden beklenen bilgiler */ }
{ eksikler . length > 0 && (
{ eksikler . length > 0 && (
< div className = "bg-amber-50 border border-amber-300 rounded-xl p-3.5 flex gap-2.5" >
< 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 > }
{ a . l } { a . zorunlu && < span className = "text-red-600" > * < / span > }
< / span >
< / span >
< input
< input
ref = { targetRef ( ` firm: ${ a . k } ` ) }
aria - invalid = { eksik || undefined }
value = { firma [ a . k ] || '' }
value = { firma [ a . k ] || '' }
onChange = { ( e ) => alanYaz ( a . k , e . target . value ) }
onChange = { ( e ) => alanYaz ( a . k , e . target . value ) }
placeholder = { a . ph || '' }
placeholder = { a . ph || '' }
@@ -319,6 +356,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
< label >
< label >
< span className = "text-[11px] font-semibold text-slate-600 block mb-1" > Teklif türü < / span >
< span className = "text-[11px] font-semibold text-slate-600 block mb-1" > Teklif türü < / span >
< select
< select
ref = { targetRef ( 'bidType' ) }
value = { teklifTuru }
value = { teklifTuru }
onChange = { ( e ) => setTeklifTuru ( e . target . value ) }
onChange = { ( e ) => setTeklifTuru ( e . target . value ) }
className = "w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white"
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 >
< label >
< span className = "text-[11px] font-semibold text-slate-600 block mb-1" > Teklif geçerlilik ( gün ) < / span >
< span className = "text-[11px] font-semibold text-slate-600 block mb-1" > Teklif geçerlilik ( gün ) < / span >
< input
< input
ref = { targetRef ( 'validity' ) }
value = { gecerlilikGun }
value = { gecerlilikGun }
onChange = { ( e ) => setGecerlilikGun ( e . target . value . replace ( /\D/g , '' ) ) }
onChange = { ( e ) => setGecerlilikGun ( e . target . value . replace ( /\D/g , '' ) ) }
className = "w-full text-xs px-3 py-2 rounded-lg border border-slate-300"
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 >
< / label >
< / div >
< / 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 }
< 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 >
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 && < >
{ partialBid . enabled && < >
@@ -368,6 +407,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
< / > }
< / > }
< / section >
< / section >
< section ref = { targetRef ( 'pricing' ) } tabIndex = { - 1 } aria - label = "Teklif fiyatlandı rma" >
{ /* Fiyatlandı rma */ }
{ /* Fiyatlandı rma */ }
{ teklifTuru === 'goturu' ? (
{ teklifTuru === 'goturu' ? (
< label className = "block" >
< label className = "block" >
@@ -375,6 +415,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
Teklif edilen toplam bedel ( KDV hariç , TL )
Teklif edilen toplam bedel ( KDV hariç , TL )
< / span >
< / span >
< input
< input
ref = { targetRef ( 'lumpSum' ) }
value = { goturuBedel }
value = { goturuBedel }
onChange = { ( e ) => setGoturuBedel ( e . target . value ) }
onChange = { ( e ) => setGoturuBedel ( e . target . value ) }
placeholder = "0"
placeholder = "0"
@@ -399,14 +440,24 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
{ satirlar . map ( ( r , i ) => {
{ satirlar . map ( ( r , i ) => {
const { miktar , birim } = miktarAyristir ( r . quantity ) ;
const { miktar , birim } = miktarAyristir ( r . quantity ) ;
const tutar = bidRowTotal ( r ) ;
const tutar = bidRowTotal ( r ) ;
const invalidRow = validateBidRows ( [ r ] ) . length > 0 ;
const errorId = ` ${ fieldPrefix } -price-error- ${ i } ` ;
return (
return (
< tr key = { r . priceKey } className = "border-b border-slate-100" >
< 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" >
< td className = "py-1.5 text-right text-slate-600 whitespace-nowrap" >
{ miktar ? ? '—' } { birim }
{ miktar ? ? '—' } { birim }
< / td >
< / td >
< td className = "py-1.5 text-right" >
< td className = "py-1.5 text-right" >
< input
< 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 ] ? ? '' }
value = { fiyatlar [ r . priceKey ] ? ? '' }
onChange = { ( e ) =>
onChange = { ( e ) =>
setFiyatlar ( ( o ) => ( { ... o , [ r . priceKey ] : e . target . value } ) ) }
setFiyatlar ( ( o ) => ( { ... o , [ r . priceKey ] : e . target . value } ) ) }
@@ -436,6 +487,8 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
< / p >
< / 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 }
< 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 ) ) } / >
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" >
{ 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 >
< ul className = "text-xs list-disc pl-5 mt-2 space-y-1" > { kontrolEksikleri . map ( ( message , i ) => < li key = { i } > { message } < / li > ) } < / ul >
< / details > }
< / details > }
< / section >
{ /* Toplam */ }
{ /* Toplam */ }
< div className = "bg-slate-900 text-white rounded-xl p-4 space-y-1" >
< 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" >
< div className = "flex items-baseline justify-between gap-3 flex-wrap" >