diff --git a/docs/quality/evidence/qa-lot-selection.txt b/docs/quality/evidence/qa-lot-selection.txt
new file mode 100644
index 0000000..c3d43dd
--- /dev/null
+++ b/docs/quality/evidence/qa-lot-selection.txt
@@ -0,0 +1,83 @@
+
+ - checkbox "Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum" [checked]
+ - text: Yalnız seçtiğim resmi kısımlara teklif hazırlıyorum
+ - text: Kısmi teklif izni — resmi doküman ve madde
+ - textbox "Kısmi teklif izni — resmi doküman ve madde": Sentetik QA örneği madde 19
+ - paragraph: 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.
+ - checkbox "Kısım 1" [checked]
+ - text: Kısım 1
+ - generic: Sentetik test kalemi
+ - generic: Resmi kısım
+ - textbox "Sentetik test kalemi resmi kısım": "1"
+ - heading "Kalem fiyatlandırma" [level=5]
+ - paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.
+ - table:
+ - rowgroup:
+ - row "Kalem Miktar Birim fiyat Tutar":
+ - columnheader "Kalem"
+ - columnheader "Miktar"
+ - columnheader "Birim fiyat"
+ - columnheader "Tutar"
+ - rowgroup:
+ - row "Sentetik test kalemi 2 adet 125,50 251,00":
+ - cell "Sentetik test kalemi"
+ - cell "2 adet"
+ - cell "125,50":
+ - textbox "0": 125,50
+ - cell "251,00"
+ - region "Belge ve ürün uygunluğu":
+ - heading "Belge ve ürün uygunluğu" [level=5]
+ - paragraph: Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.
+ - group "Sentetik yetki belgesi (zorunlu)":
+ - generic: Sentetik yetki belgesi (zorunlu)
+ - paragraph: Yerel QA dokümanı madde 7
+ - button "Elle eklenen gereksinimi kaldır"
+ - combobox "Kontrol durumu":
+ - option "Kontrol gerekli"
+ - option "Doğrulandı" [selected]
+ - text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
+ - textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7
+ - paragraph: Resmi dokümandan eksik belge gereksinimi ekle
+ - text: Belge adı
+ - textbox "Belge adı"
+ - text: Kaynak doküman ve madde
+ - textbox "Kaynak doküman ve madde"
+ - button "Belge gereksinimi ekle" [disabled]
+ - generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü"
+ - option "Kontrol gerekli"
+ - option "Uygun" [selected]
+ - option "Uygun değil"
+ - option "Kontrol gerekli"
+ - option "Doğrulandı"
+ - option "Uygulanmıyor — gerekçe gerekli" [selected]
+ - option "Kontrol gerekli"
+ - option "Doğrulandı"
+ - option "Uygulanmıyor — gerekçe gerekli" [selected]
+ - option "Kontrol gerekli"
+ - option "Doğrulandı"
+ - option "Uygulanmıyor — gerekçe gerekli" [selected]
+ - option "Kontrol gerekli"
+ - option "Doğrulandı"
+ - option "Uygulanmıyor — gerekçe gerekli" [selected]
+ - generic: Teklif toplamı (KDV hariç)
+ - generic: 251,00 TL
+ - paragraph: "Yazı ile: ikiyüzellibir Türk Lirası"
+ - heading "Hazırlanacak belgeler" [level=5]
+ - button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]:
+ - generic: Teklif Mektubu
+ - generic: Rakam ve yazı ile bedel
+ - text: Yazdır / PDF
+ - button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]:
+ - generic: Birim Fiyat Teklif Cetveli
+ - generic: 1 kalem
+ - text: Yazdır / PDF
+ - button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]:
+ - generic: Evrak Kontrol Listesi
+ - generic: 1 belge
+ - text: Yazdır / PDF
+ - paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın.
+ - paragraph: 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.
+- button
+- button "Kullanım kılavuzu (? tuşu)":
+- region "Notifications (F8)":
+ - list
\ No newline at end of file
diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md
index 690998f..37c1806 100644
--- a/docs/quality/uygulama-durumu.md
+++ b/docs/quality/uygulama-durumu.md
@@ -71,9 +71,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- Düzeltme sonrasında tarayıcıda `125,50 × 2 = 251,00 TL` görüldü ve yeniden yüklemede korundu. Bir otomatik test ekran hesabı/PDF HTML tutarlılığı, geçersiz sayı ve aşırı tutarı kapsar.
- Kanıtlar: [taslak yenilemesi](evidence/qa-draft-after-reload.txt), [virgüllü fiyat](evidence/qa-turkish-price-fixed.txt), [kontrol yenilemesi](evidence/qa-checks-after-reload.txt).
+## Kısmi teklif hazırlığı
+
+- Kısmi teklif izninin doküman/madde kaydı, resmi kısım eşleştirmesi ve kısım seçimi eklendi. Bilinmeyen kısım otomatik tahmin edilmez; kaynakta kısım varsa kullanıcı üzerine yazamaz.
+- Aynı kısma ait tüm satırlar birlikte seçilir. Yalnız seçili satırlar fiyat, ürün uygunluğu ve cetvel çıktısına girer. Kısım değişince kaynak karşılaştırması yeniden gerekir; önceki fiyatlar kendi satırında korunur.
+- Seçim, eşleştirmeler ve izin kaynağı hazırlık taslağına kaydedilir. Eski seçili kısım güncel kaynakta yoksa çıktı engellenir.
+- İki test seçilmeyen 1.000 TL cihazın 80 TL'lik seçili cetvele karışmamasını, eksik kaynak/eşleştirme ve kaybolan kısım engellerini doğrular. Tarayıcıda tek sentetik satır için boş seçim engeli, kısım eşleştirmesi ve seçili fiyat gösterimi doğrulandı: [kanıt](evidence/qa-lot-selection.txt).
+- Aksaray'ın gerçek 34 kısmının resmi dokümanıyla karşılaştırma ve çok kısımlı tarayıcı kabulü henüz yoktur. Götürü bedel tutarı kullanıcı tarafından seçilen kapsam için ayrıca girilir.
+
## Doğrulama
-`npm test`: 31/31 geçti (son yeniden çalıştırma: 6 Eylül 2026).
+`npm test`: 33/33 geçti (son yeniden çalıştırma: 6 Eylül 2026).
`npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor.
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx
index dd0127d..49e62a9 100644
--- a/src/components/BidPackageBuilder.jsx
+++ b/src/components/BidPackageBuilder.jsx
@@ -1,6 +1,7 @@
import React, { useState, useMemo } from 'react';
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
+import { rowLot, selectBidLots } from '../services/bidLots';
import { PreparationChecks } from './PreparationChecks';
import { preparationBlockers, documentKey } from '../services/preparationChecks';
import { useApp } from '../context/AppContext';
@@ -66,8 +67,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
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);
- const sourceSnapshot = bidSourceSnapshot(tender, {...effectiveResults, documentChecklist:documents}, kalemler);
+ 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]);
const [reviewedSource, setReviewedSource] = useState(storedDraft.reviewedSource || null);
const [fiyatlar, setFiyatlar] = useState(() => restoreBidPrices(storedDraft));
const kaynakKontrolGerekli = reviewedSource !== sourceSnapshot;
@@ -80,13 +84,13 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
};
const satirlar = useMemo(
- () => kalemler.map(k => ({ ...k, birimFiyat: fiyatlar[k.priceKey] })),
- [kalemler, fiyatlar],
+ () => 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 saveDraft = async () => {
- const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}});
+ const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null}}});
if (saved) { setDirty(false); setHata(null); }
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
};
@@ -97,14 +101,14 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
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(kalemler, documents, checks);
- const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0;
+ const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks);
+ const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0;
const formVerisi = () => ({
...firma,
teklifTuru,
gecerlilikGun,
- kisim,
+ kisim: partialBid.enabled ? partialBid.selected.join(', ') : kisim,
idare: tender?.kurum || '',
isinAdi: tender?.baslik || '',
ikn: tender?.ikn || '',
@@ -238,6 +242,27 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
+ 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.
@@ -302,11 +327,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {- 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. + {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.'}
)} -