Files
ihaleist/src/components/YeterlilikRozeti.jsx
T
hikmettanandClaude Fable 5.1 48f0c48732 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>
2026-09-04 02:23:41 +03:00

63 lines
3.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
};