feat: sinir deger hesaplayici — Tebliğ md. 79/45 formulleri, 2026 R katsayilari

Formuller kaynagindan alindi (mevzuat.gov.tr Kamu Ihale Genel Tebligi,
kullanicinin Chrome'u uzerinden okundu; formuller Teblig'de gorsel oldugu icin
ekran goruntusunden):
- Md. 79.1.2 hizmet (personel dayali olmayan): SD = ((YM + ΣT)/(n+1)) × R,
  YM'nin %60 alti ve YM ustu teklifler haric
- Md. 79.1.1 personel calistirmaya dayali: SD = kar haric YM
- Md. 62/A.1 (RG 9/4/2026): mal aliminda 79 uygulanir
- Md. 45.1.1 yapim: Tort1/σ/Tort2, C = Tort2/YM, K parcali, SD = K×Tort2/(C×N);
  N 1,00 (B/C/D/E gruplari) veya 1,20 (45.1.1.1)
- R katsayilari: KIK Kurul karari 21.01.2026 (.doc'tan okundu): arac kiralama
  0,77, bilgisayar 0,87, yemek 0,77, muhendislik 0,72, sigorta 0,74, tasima
  0,78, diger 0,78

Dogrulama: Teblig'in kendi 45.2 ornegi (YM 100.000, N 1,20, 12 teklif) ile
birebir — Tort1 66.270,00, σ 20.317,05, Tort2 63.237,50, C 0,632, K 0,627,
SD 52.281,00. Yuvarlama kurallari ornekteki gibi (2/3 ondalik).

Arac: ust menude "Sinir Deger"; alim turu, YM, teklif listesi, R/N secimi,
"benim teklifim sinirin altinda mi" uyarisi, adim adim hesap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 00:16:58 +03:00
co-authored by Claude Fable 5.1
parent df89eaac33
commit 91b8ea94b8
4 changed files with 264 additions and 0 deletions
+2
View File
@@ -21,6 +21,7 @@ import { ResultSearch } from './components/tools/ResultSearch';
import { TenderCalendar } from './components/tools/TenderCalendar'; import { TenderCalendar } from './components/tools/TenderCalendar';
import { ContractSearch } from './components/tools/ContractSearch'; import { ContractSearch } from './components/tools/ContractSearch';
import { VatCalculator } from './components/tools/VatCalculator'; import { VatCalculator } from './components/tools/VatCalculator';
import { SinirDeger } from './components/tools/SinirDeger';
import { PriceAnalysis } from './components/tools/PriceAnalysis'; import { PriceAnalysis } from './components/tools/PriceAnalysis';
import { CompetitorRadar } from './components/tools/CompetitorRadar'; import { CompetitorRadar } from './components/tools/CompetitorRadar';
import { DocumentVault } from './components/tools/DocumentVault'; import { DocumentVault } from './components/tools/DocumentVault';
@@ -61,6 +62,7 @@ const MainContent = () => {
{activeTab === 'takvim' && <TenderCalendar />} {activeTab === 'takvim' && <TenderCalendar />}
{activeTab === 'sozlesme' && <ContractSearch />} {activeTab === 'sozlesme' && <ContractSearch />}
{activeTab === 'kdv' && <VatCalculator />} {activeTab === 'kdv' && <VatCalculator />}
{activeTab === 'sinir' && <SinirDeger />}
{activeTab === 'fiyat' && <PriceAnalysis />} {activeTab === 'fiyat' && <PriceAnalysis />}
{activeTab === 'kanban' && <KanbanBoard />} {activeTab === 'kanban' && <KanbanBoard />}
{activeTab === 'rakip_radar' && <CompetitorRadar />} {activeTab === 'rakip_radar' && <CompetitorRadar />}
+1
View File
@@ -25,6 +25,7 @@ export const TopNavigation = () => {
{ id: 'takvim', label: 'İhale Takvimi', icon: CalendarDays }, { id: 'takvim', label: 'İhale Takvimi', icon: CalendarDays },
{ id: 'sozlesme', label: 'Sözleşme Sorgula', icon: FileCheck2 }, { id: 'sozlesme', label: 'Sözleşme Sorgula', icon: FileCheck2 },
{ id: 'kdv', label: 'KDV Hesapla', icon: Calculator }, { id: 'kdv', label: 'KDV Hesapla', icon: Calculator },
{ id: 'sinir', label: 'Sınır Değer', icon: Calculator },
{ id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp }, { id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp },
{ id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' }, { id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' },
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' }, { id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' },
+156
View File
@@ -0,0 +1,156 @@
import React, { useMemo, useState } from 'react';
import { Scale, AlertTriangle, CheckCircle2, Info } from 'lucide-react';
import { sinirDegerHizmet, sinirDegerPersonel, sinirDegerYapim, teklifleriAyir, R_2026, R_KAYNAK } from '../../services/sinirDeger';
/**
* Aşırı düşük teklif sınır değeri hesaplayıcı.
* Formüller Kamu İhale Genel Tebliği md. 79 (hizmet/mal) ve 45 (yapım),
* R katsayıları KİK 21.01.2026 Kurul kararı — kaynaklar services/sinirDeger.js'de.
* Teklifçinin sorusu: "Teklifim sınırın altında kalıp aşırı düşük sorgusuna girer mi?"
*/
const tl = (n) => (Number.isFinite(n) ? n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' TL' : '—');
export const SinirDeger = () => {
const [tur, setTur] = useState('hizmet'); // hizmet | mal | personel | yapim
const [ym, setYm] = useState('');
const [tekliflerMetin, setTekliflerMetin] = useState('');
const [rSecim, setRSecim] = useState(String(R_2026[6].r));
const [rOzel, setROzel] = useState('');
const [nKat, setNKat] = useState('1.2');
const [benimTeklif, setBenimTeklif] = useState('');
const teklifler = useMemo(() => teklifleriAyir(tekliflerMetin), [tekliflerMetin]);
const r = rSecim === 'ozel' ? Number(String(rOzel).replace(',', '.')) : Number(rSecim);
const sonuc = useMemo(() => {
if (tur === 'personel') return ym ? sinirDegerPersonel({ karHaricYm: ym }) : null;
if (!ym || !teklifler.length) return null;
if (tur === 'yapim') return sinirDegerYapim({ ym, teklifler, n: Number(nKat) });
return sinirDegerHizmet({ ym, teklifler, r });
}, [tur, ym, teklifler, r, nKat]);
const benim = teklifleriAyir(benimTeklif)[0];
const altinda = sonuc?.sd != null && Number.isFinite(benim) ? benim < sonuc.sd : null;
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-emerald-950 to-teal-900 text-white p-6 rounded-2xl shadow-lg border border-emerald-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-emerald-600 flex items-center justify-center shadow-md"><Scale className="w-6 h-6" /></div>
<div>
<h2 className="font-extrabold text-xl">Sınır Değer Hesaplayıcı</h2>
<p className="text-xs text-emerald-200 mt-0.5">Teklifiniz aşırı düşük teklif sorgusuna girer mi? Kamu İhale Genel Tebliği md. 79 / 45 formülleriyle.</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-4">
<div>
<span className="text-[11px] font-bold text-slate-600 block mb-1.5">Alım türü</span>
<div className="grid grid-cols-2 gap-1.5">
{[['hizmet', 'Hizmet (personel dayalı olmayan)'], ['mal', 'Mal alımı'], ['personel', 'Hizmet (personel çalıştırmaya dayalı)'], ['yapim', 'Yapım işi']].map(([k, ad]) => (
<button key={k} type="button" onClick={() => setTur(k)}
className={`text-xs font-bold px-3 py-2 rounded-lg border text-left ${tur === k ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-white text-slate-700 border-slate-300 hover:bg-slate-50'}`}>
{ad}
</button>
))}
</div>
{tur === 'mal' && <p className="text-[11px] text-slate-500 mt-1.5">Md. 62/A.1 (RG 9/4/2026): mal alımında aşırı düşük değerlendirmesi 79 uncu maddeye göre yapılır — hizmet formülü uygulanır.</p>}
</div>
<label className="block">
<span className="text-[11px] font-bold text-slate-600 block mb-1">{tur === 'personel' ? 'Kâr hariç yaklaşık maliyet (TL)' : 'Yaklaşık maliyet — YM (TL)'}</span>
<input value={ym} onChange={(e) => setYm(e.target.value)} placeholder="Örn. 1.000.000,00"
className="w-full text-sm px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none" />
<span className="text-[10px] text-slate-400">İhale komisyonu yaklaşık maliyeti ilk oturumda açıklar; teklif öncesi tahminle deneyebilirsiniz.</span>
</label>
{tur !== 'personel' && (
<label className="block">
<span className="text-[11px] font-bold text-slate-600 block mb-1">Geçerli teklifler (her satıra bir tutar)</span>
<textarea value={tekliflerMetin} onChange={(e) => setTekliflerMetin(e.target.value)} rows={6}
placeholder={'850.000\n900.000\n920.000\n…'}
className="w-full text-sm px-3 py-2 rounded-lg border border-slate-300 font-mono focus:ring-2 focus:ring-emerald-500 outline-none" />
<span className="text-[10px] text-slate-400">{teklifler.length} teklif okundu · teklif mektubu ve geçici teminatı usulüne uygun olanlar</span>
</label>
)}
{(tur === 'hizmet' || tur === 'mal') && (
<div>
<span className="text-[11px] font-bold text-slate-600 block mb-1">R katsayısı (2026)</span>
<select value={rSecim} onChange={(e) => setRSecim(e.target.value)} className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white">
{R_2026.map((x) => <option key={x.ad} value={String(x.r)}>{x.ad} — {x.r.toLocaleString('tr-TR')}</option>)}
<option value="ozel">Özel değer gir…</option>
</select>
{rSecim === 'ozel' && (
<input value={rOzel} onChange={(e) => setROzel(e.target.value)} placeholder="0,78"
className="mt-1.5 w-32 text-sm px-3 py-1.5 rounded-lg border border-slate-300" />
)}
<p className="text-[10px] text-slate-400 mt-1">{R_KAYNAK}. Birden fazla hizmet konusu varsa YM içinde ağırlıklı olana göre.</p>
</div>
)}
{tur === 'yapim' && (
<div>
<span className="text-[11px] font-bold text-slate-600 block mb-1">Sınır değer katsayısı N (md. 45.1.1.1)</span>
<select value={nKat} onChange={(e) => setNKat(e.target.value)} className="w-full text-xs px-3 py-2 rounded-lg border border-slate-300 bg-white">
<option value="1">1,00 — B üstyapı (bina), C sıhhi/mekanik tesisat, D elektrik, E elektronik-iletişim işleri</option>
<option value="1.2">1,20 — diğer yapım işleri</option>
</select>
</div>
)}
<label className="block">
<span className="text-[11px] font-bold text-slate-600 block mb-1">Sizin teklifiniz (TL) — isteğe bağlı</span>
<input value={benimTeklif} onChange={(e) => setBenimTeklif(e.target.value)} placeholder="Örn. 780.000"
className="w-full text-sm px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none" />
</label>
</div>
<div className="space-y-4">
{!sonuc && (
<div className="bg-slate-50 border border-dashed border-slate-300 rounded-2xl p-8 text-center text-xs text-slate-500">
Yaklaşık maliyeti ve teklifleri girin; sınır değer ve adımları burada görünür.
</div>
)}
{sonuc?.hata && (
<div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex gap-2 text-xs text-red-800"><AlertTriangle className="w-4 h-4 shrink-0" />{sonuc.hata}</div>
)}
{sonuc && !sonuc.hata && (
<>
<div className="bg-slate-900 text-white rounded-2xl p-5 space-y-2">
<p className="text-[11px] font-bold text-slate-300 uppercase tracking-wider">Aşırı düşük teklif sınır değeri</p>
<p className="text-3xl font-black text-emerald-300">{tl(sonuc.sd)}</p>
{altinda != null && (
<div className={`mt-2 rounded-xl p-3 text-xs flex gap-2 ${altinda ? 'bg-amber-500/20 border border-amber-400/40 text-amber-100' : 'bg-emerald-500/20 border border-emerald-400/40 text-emerald-100'}`}>
{altinda ? <AlertTriangle className="w-4 h-4 shrink-0" /> : <CheckCircle2 className="w-4 h-4 shrink-0" />}
<span>
Teklifiniz <strong>{tl(benim)}</strong> — sınırın {altinda ? <><strong>altında</strong>: aşırı düşük teklif açıklaması istenir (md. 79.2 / 45.1.2), maliyet bileşenlerini belgeyle savunmaya hazır olun.</> : <><strong>üstünde</strong>: aşırı düşük sorgusuna girmez.</>}
</span>
</div>
)}
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-2">
<h3 className="font-bold text-sm text-slate-900">Hesap adımları</h3>
<ol className="space-y-1.5 text-xs text-slate-700 list-decimal pl-5">
{sonuc.adimlar.map((a, i) => <li key={i} className="leading-relaxed">{a}</li>)}
</ol>
{sonuc.haric?.length > 0 && (
<p className="text-[11px] text-slate-500">Hesap dışı bırakılan teklifler: {sonuc.haric.map((x) => x.toLocaleString('tr-TR')).join(', ')}</p>
)}
</div>
</>
)}
<div className="bg-blue-50 border border-blue-200 rounded-2xl p-4 flex gap-2.5 text-[11px] text-blue-900 leading-relaxed">
<Info className="w-4 h-4 shrink-0 mt-0.5" />
<span>
Formüller Kamu İhale Genel Tebliği md. 79.1.1–79.1.2 (hizmet), 62/A.1 (mal) ve 45.1.1 (yapım) metninden alındı; yapım hesabı Tebliğ'in 45.2 örneğiyle (YM 100.000, N 1,20 → 52.281,00 TL) birebir doğrulandı.
Nihai sınır değeri ihale komisyonu tespit eder; bu hesap teklif stratejisi içindir.
</span>
</div>
</div>
</div>
</div>
);
};
+105
View File
@@ -0,0 +1,105 @@
/**
* Aşırı düşük teklif sınır değeri — Kamu İhale Genel Tebliği'nden.
*
* KAYNAK (mevzuat.gov.tr, 9.5.13354, 2026-09-04'te okundu):
* - Md. 79.1.1 Personel çalıştırılmasına dayalı hizmet: SD = kâr hariç yaklaşık maliyet.
* - Md. 79.1.2 Dayalı olmayan hizmet: SD = ((YM + T1 + … + Tn) / (n + 1)) × R
* n: geçerli teklifler içinde YM'nin %60'ından düşük ve YM'den
* yüksek olanlar DIŞINDAKİ teklif sayısı. (Formül Tebliğ'de
* görsel; ekran görüntüsünden okundu.)
* - Md. 79.1.3 R her yıl 1 Şubat'tan geçerli Kurum'ca ilan edilir.
* - Md. 62/A.1 (Değişik RG 9/4/2026) Mal alımında aşırı düşük değerlendirmesi
* 79 uncu maddeye göre yapılır → mal alımı da bu formülü kullanır.
* - Md. 45.1.1 Yapım: YM'nin %120 üstü ve %40 altı hariç geçerli tekliflerin
* Tort1 ve σ'sı; [Tort1−σ, Tort1+σ] içindekilerin Tort2'si;
* C = Tort2/YM; K: C<0,60 → K=C; 0,60≤C≤1,00 → (3,2C−C²−0,6)/(C+1);
* C>1,00 → (C²−0,8C+1,4)/(C+1); SD = K×Tort2/(C×N).
* N (45.1.1.1): B/C/D/E grubu yapım işleri 1,00; diğer 1,20.
* Yuvarlama (45.2 örneği): Tort1, σ, Tort2 2 ondalık; C, K 3 ondalık.
*
* R KATSAYILARI 2026 (KİK Kurul kararı 21.01.2026, ilanı 1/2/2026+ ihaleler):
*/
export const R_2026 = [
{ ad: 'Araç Kiralama Hizmetleri', r: 0.77 },
{ ad: 'Bilgisayar Sistemlerine Yönelik Hizmetler', r: 0.87 },
{ ad: 'Malzemeli Yemek Hizmetleri', r: 0.77 },
{ ad: 'Mühendislik Hizmetleri', r: 0.72 },
{ ad: 'Sigorta Hizmetleri', r: 0.74 },
{ ad: 'Personel/Öğrenci Taşıma Hizmetleri', r: 0.78 },
{ ad: 'Diğer Hizmetler', r: 0.78 },
];
export const R_KAYNAK = 'KİK Kurul Kararı 21.01.2026 — ilanı 1/2/2026 ve sonrası ihaleler';
const yuvarla = (x, b) => Math.round((x + Number.EPSILON) * 10 ** b) / 10 ** b;
const sayi = (v) => {
if (typeof v === 'number') return v;
const s = String(v ?? '').trim().replace(/\s|TL|₺/gi, '');
if (!s) return NaN;
// "1.500.000,50" (TR) ya da "1500000.50"
const tr = s.includes(',') ? s.replace(/\./g, '').replace(',', '.') : s.replace(/\.(?=\d{3}(\D|$))/g, '');
return Number(tr);
};
export const teklifleriAyir = (metin) =>
String(metin || '').split(/[\n;]+/).map((x) => sayi(x)).filter((x) => Number.isFinite(x) && x > 0);
/** Hizmet (personel dayalı olmayan) ve mal alımı — Md. 79.1.2 */
export function sinirDegerHizmet({ ym, teklifler, r }) {
const YM = sayi(ym); const R = Number(r);
if (!(YM > 0) || !(R > 0)) return { hata: 'Yaklaşık maliyet ve R katsayısı gerekli' };
const gecerli = teklifler.filter((t) => t >= YM * 0.6 && t <= YM);
const disi = teklifler.filter((t) => t < YM * 0.6 || t > YM);
const n = gecerli.length;
const toplam = gecerli.reduce((a, b) => a + b, 0);
const ort = (YM + toplam) / (n + 1);
const sd = yuvarla(ort * R, 2);
return {
tur: 'hizmet', sd, n, r: R, ym: YM,
hesapOrtalama: yuvarla(ort, 2), dahil: gecerli, haric: disi,
adimlar: [
`Geçerli teklifler içinde YM'nin %60'ından düşük (${yuvarla(YM * 0.6, 2).toLocaleString('tr-TR')}) ve YM'den yüksek olanlar çıkarıldı: ${disi.length} teklif hariç, n = ${n}`,
`(YM + ΣT) / (n + 1) = (${YM.toLocaleString('tr-TR')} + ${toplam.toLocaleString('tr-TR')}) / ${n + 1} = ${yuvarla(ort, 2).toLocaleString('tr-TR')}`,
`SD = ${yuvarla(ort, 2).toLocaleString('tr-TR')} × R(${R}) = ${sd.toLocaleString('tr-TR')} TL`,
],
};
}
/** Personel çalıştırılmasına dayalı hizmet — Md. 79.1.1 */
export function sinirDegerPersonel({ karHaricYm }) {
const v = sayi(karHaricYm);
if (!(v > 0)) return { hata: 'Kâr hariç yaklaşık maliyet gerekli' };
return { tur: 'personel', sd: yuvarla(v, 2), adimlar: ['Md. 79.1.1: kâr hariç yaklaşık maliyet tutarı sınır değer kabul edilir.'] };
}
/** Yapım — Md. 45.1.1 */
export function sinirDegerYapim({ ym, teklifler, n: N = 1.2 }) {
const YM = sayi(ym); const Nk = Number(N);
if (!(YM > 0)) return { hata: 'Yaklaşık maliyet gerekli' };
const dahil = teklifler.filter((t) => t >= YM * 0.4 && t <= YM * 1.2);
const haric1 = teklifler.filter((t) => t < YM * 0.4 || t > YM * 1.2);
if (dahil.length < 2) return { hata: 'Aralıkta en az iki geçerli teklif olmalı' };
const tort1 = yuvarla(dahil.reduce((a, b) => a + b, 0) / dahil.length, 2);
const sigma = yuvarla(Math.sqrt(dahil.reduce((a, t) => a + (t - tort1) ** 2, 0) / (dahil.length - 1)), 2);
const alt = yuvarla(tort1 - sigma, 2), ust = yuvarla(tort1 + sigma, 2);
const bant = dahil.filter((t) => t >= alt && t <= ust);
const haric2 = dahil.filter((t) => t < alt || t > ust);
if (!bant.length) return { hata: 'Standart sapma aralığında teklif kalmadı' };
const tort2 = yuvarla(bant.reduce((a, b) => a + b, 0) / bant.length, 2);
const C = yuvarla(tort2 / YM, 3);
let K, kFormul;
if (C < 0.6) { K = C; kFormul = 'C < 0,60 → K = C'; }
else if (C <= 1.0) { K = yuvarla((3.2 * C - C * C - 0.6) / (C + 1), 3); kFormul = '0,60 ≤ C ≤ 1,00 → K = (3,2C − C² − 0,6)/(C + 1)'; }
else { K = yuvarla((C * C - 0.8 * C + 1.4) / (C + 1), 3); kFormul = 'C > 1,00 → K = (C² − 0,8C + 1,4)/(C + 1)'; }
const sd = yuvarla((K * tort2) / (C * Nk), 2);
const f = (x) => x.toLocaleString('tr-TR');
return {
tur: 'yapim', sd, ym: YM, n: Nk, tort1, sigma, alt, ust, tort2, C, K, dahil, bant, haric: [...haric1, ...haric2],
adimlar: [
`YM'nin %120 üstü (${f(YM * 1.2)}) ve %40 altı (${f(YM * 0.4)}) hariç: ${haric1.length} teklif dışarıda, ${dahil.length} teklif ortalamaya girdi`,
`Tort1 = ${f(tort1)} · σ = ${f(sigma)} · aralık [${f(alt)} , ${f(ust)}]`,
`Aralıkta kalan ${bant.length} teklif → Tort2 = ${f(tort2)} (${haric2.length} teklif aralık dışı)`,
`C = Tort2 / YM = ${C}`,
`${kFormul} → K = ${K}`,
`SD = (K × Tort2) / (C × N) = (${K} × ${f(tort2)}) / (${C} × ${Nk}) = ${f(sd)} TL`,
],
};
}