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:
@@ -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>;
|
||||
};
|
||||
Reference in New Issue
Block a user