feat: İhaleAB'den taşınan özellikler — MCP sunucusu, RSS, haftalık brief, teklif asistanı, yeterlilik rozeti, 5 yeni kırmızı bayrak

- /mcp (JSON-RPC, ih_live_ anahtarı): search_tenders, get_tender, search_results, search_items, upcoming_deadlines
- /api/feeds/tenders.rss (anahtarlı) ve /api/feeds/sonuc.rss (açık)
- Haftalık brief (Pazartesi 08:00, Telegram + e-posta), /api/me/brief tercihi + önizleme, sağ panelde yaklaşan ihale tarihleri
- Detayda Teklif Asistanı sekmesi: belgeye sor (atıflı), taslak denetimi, taslak üretimi (analyzeSpec zinciri)
- Derin analiz belge listesi × Doküman Deposu eşleştirme rozeti
- Kırmızı bayrak: kısa süre, hafta sonu tarih, geç sözleşme, kazananı gizli, DT parçalama şüphesi
- Kullanım kılavuzu güncellendi

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 02:23:41 +03:00
co-authored by Claude Fable 5.1
parent fabff18fe9
commit 48f0c48732
18 changed files with 940 additions and 10 deletions
+60
View File
@@ -204,6 +204,7 @@ export function redFlags(tenders) {
// Firma-kurum yoğunlaşması için sayaçlar
const firmKurum = new Map(); // "firma||kurum" -> sayı
const kurumToplam = new Map(); // kurum -> sonuçlanan ihale sayısı
const dtKurum = new Map(); // "kurum||sektor" -> [{t, d}] (doğrudan temin parçalama)
for (const t of tenders) {
const bedel = parseTL(t.sozlesmeBedeli);
@@ -247,6 +248,48 @@ export function redFlags(tenders) {
}
}
// 6) Kısa süre: ilan → ihale tarihi 7 günden az (açık ihalelerde)
const ilanT = parseDateTR(t.ilanTarihi);
const ihaleT = parseDateTR(t.tarih);
if (!t.kazananFirma && ilanT && ihaleT && ihaleT > ilanT && (ihaleT - ilanT) / 86400000 < 7) {
flags.push({
id: `sure-${t.id}`, seviye: 'orta', tur: 'Kısa süre', tender: t,
deger: `${Math.round((ihaleT - ilanT) / 86400000)} gün`,
aciklama: 'İlan ile ihale tarihi arasında 7 günden az var — hazırlık süresi yetersiz, katılım daralır.',
});
}
// 7) Hafta sonu ihale tarihi
if (!t.kazananFirma && ihaleT && (ihaleT.getDay() === 0 || ihaleT.getDay() === 6)) {
flags.push({
id: `hsonu-${t.id}`, seviye: 'dusuk', tur: 'Hafta sonu tarih', tender: t,
deger: ihaleT.getDay() === 6 ? 'Cumartesi' : 'Pazar',
aciklama: 'İhale tarihi hafta sonuna düşüyor; idare kapalıyken soru sorulamaz, teslim riski artar.',
});
}
// 8) Geç sözleşme: ihale tarihinden 180+ gün sonra sözleşme
const sozT = parseDateTR(t.sozlesmeTarihi);
if (sozT && ihaleT && (sozT - ihaleT) / 86400000 > 180) {
flags.push({
id: `gec-${t.id}`, seviye: 'orta', tur: 'Geç sözleşme', tender: t,
deger: `${Math.round((sozT - ihaleT) / 86400000)} gün`,
aciklama: 'Sözleşme ihaleden 180 günden uzun süre sonra imzalanmış — uzun değerlendirme, teklif geçerlilik ve fiyat riski.',
});
}
// 9) Kazananı açıklanmamış sonuç
if (t.durum === 'Sonuçlandı' && !t.kazananFirma && (bedel != null || t.teklifSayisi != null)) {
flags.push({
id: `gizli-${t.id}`, seviye: 'orta', tur: 'Kazananı gizli', tender: t,
aciklama: 'Sonuç ilanı var ama kazanan firma adı verilmemiş — şeffaflık eksiği.',
});
}
// 10) Parçalama şüphesi (sayaç): aynı idare, aynı sektör, 30 günde 3+ doğrudan temin
if (/doğrudan|dt\b/i.test(String(t.alimTuru || t.usul || '')) && t.kurum && ihaleT) {
const key = `${t.kurum.trim()}||${t.sektor || ''}`;
const arr = dtKurum.get(key) || [];
arr.push({ t, d: ihaleT });
dtKurum.set(key, arr);
}
if (t.kazananFirma && t.kurum) {
const key = `${t.kazananFirma.trim()}||${t.kurum.trim()}`;
firmKurum.set(key, (firmKurum.get(key) || 0) + 1);
@@ -267,6 +310,23 @@ export function redFlags(tenders) {
}
}
// 10) Parçalama: 30 günlük pencerede aynı idare + sektörde 3+ doğrudan temin
for (const [key, arr] of dtKurum) {
if (arr.length < 3) continue;
arr.sort((a, b) => a.d - b.d);
for (let i = 0; i + 2 < arr.length; i++) {
if ((arr[i + 2].d - arr[i].d) / 86400000 <= 30) {
const [kurum, sektor] = key.split('||');
flags.push({
id: `parca-${key}-${arr[i].t.id}`, seviye: 'yuksek', tur: 'Parçalama şüphesi', tender: arr[i].t, kurum,
deger: `${arr.length} DT / 30 gün`,
aciklama: `${kurum}, ${sektor || 'aynı'} alanında 30 gün içinde ${arr.length} doğrudan temin yapmış — ihaleye çıkmaktan kaçınma (parçalama) şüphesi, 4734 md. 5 ve 22/d sınırı.`,
});
break;
}
}
}
const siraDeger = { yuksek: 0, orta: 1, dusuk: 2 };
flags.sort((a, b) => siraDeger[a.seviye] - siraDeger[b.seviye]);
return {