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
+62
View File
@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import { listDocuments } from '../services/account';
/**
* Derin analiz belge kontrol listesi × Doküman Deposu eşleştirmesi.
* Belge adı/kategori sözcüklerine göre depodaki evrakı bulur: Hazır /
* Yakında dolacak (≤30 gün) / Süresi dolmuş / Eksik. Depo listesi bir kez
* çekilip modül düzeyinde paylaşılır.
*/
const KATEGORI_DESEN = {
'Yasal Belgeler': /imza|sirk|vekalet|yetki|sicil|gazete|oda\s*kay|faaliyet|ticaret|kuruluş|adli|sabıka|iflas|konkordato/i,
'Kalite Belgeleri': /iso|kalite|tse|ce\b|sertifika|belgelendirme|9001|14001|45001|27001|hizmet\s*yeterlilik/i,
'Mali Belgeler': /bilanço|mali|gelir|ciro|banka|referans|teminat|vergi\s*borc|sgk|prim|sosyal\s*güvenlik/i,
'Resmi Kayıtlar': /vergi\s*levha|kimlik|ekap\s*kay|kayıt\s*belge|mersis|ticaret\s*sicil\s*no/i,
'Teknik Belgeler': /teknik|katalog|test\s*rapor|numune|uygunluk|garanti|referans\s*iş|iş\s*deneyim|iş\s*bitirme|kapasite|üretim/i,
};
let depoCache = null;
let depoPromise = null;
function depoyuGetir() {
if (depoCache) return Promise.resolve(depoCache);
if (!depoPromise) depoPromise = listDocuments().then((d) => (depoCache = d.belgeler || d.documents || [])).catch(() => (depoCache = []));
return depoPromise;
}
const norm = (s) => String(s ?? '').toLocaleLowerCase('tr-TR').replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
function benzerlik(a, b) {
const x = norm(a).split(' ').filter((k) => k.length >= 4);
if (!x.length) return 0;
const y = norm(b);
return x.filter((k) => y.includes(k)).length / x.length;
}
export function eslestir(belge, depo, bugun = new Date()) {
const kat = Object.entries(KATEGORI_DESEN).find(([, re]) => re.test(norm(belge)))?.[0] || null;
const aday = depo
.map((d) => ({ d, puan: (kat && d.kategori === kat ? 1 : 0) + benzerlik(belge, d.ad) }))
.filter((x) => x.puan >= 0.5)
.sort((a, b) => b.puan - a.puan)[0]?.d;
if (!aday) return { durum: 'eksik', kategori: kat, belge: null, kalan: null };
const son = aday.sonKullanma ? new Date(aday.sonKullanma) : null;
const kalan = son && !Number.isNaN(son.getTime()) ? Math.floor((son - bugun) / 86400000) : null;
const durum = kalan != null && kalan < 0 ? 'suresi_dolmus' : kalan != null && kalan <= 30 ? 'yakinda' : 'hazir';
return { durum, kategori: kat, belge: aday, kalan };
}
const GORUNUM = {
hazir: ['Depoda hazır', 'bg-emerald-100 text-emerald-800'],
yakinda: ['Yakında dolacak', 'bg-amber-100 text-amber-800'],
suresi_dolmus: ['Süresi dolmuş', 'bg-red-100 text-red-800'],
eksik: ['Depoda yok', 'bg-slate-200 text-slate-700'],
};
export const YeterlilikRozeti = ({ belge }) => {
const [depo, setDepo] = useState(depoCache);
useEffect(() => { if (!depo) depoyuGetir().then(setDepo); }, [depo]);
if (!depo || !belge) return null;
const e = eslestir(belge, depo);
const [etiket, sinif] = GORUNUM[e.durum];
const ipucu = e.belge ? `${e.belge.ad}${e.kalan != null ? ` · ${e.kalan < 0 ? -e.kalan + ' gün önce doldu' : e.kalan + ' gün kaldı'}` : ''}` : e.kategori ? `Doküman Deposu'na "${e.kategori}" kategorisinde yükleyin` : 'Depoda eşleşen belge yok';
return <span title={ipucu} className={`text-[10px] font-bold px-1.5 py-0.5 rounded shrink-0 ${sinif}`}>{etiket}</span>;
};