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:
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { listApiKeys, createApiKey, revokeApiKey } from '../../services/account';
|
||||
import { Webhook, Key, Plus, Trash2, Loader2, Copy, Check, AlertTriangle, ShieldCheck, Lock } from 'lucide-react';
|
||||
import { McpRssKarti } from './McpRssKarti';
|
||||
|
||||
/**
|
||||
* Gerçek API anahtarı yönetimi.
|
||||
@@ -154,6 +155,8 @@ export const ApiWebhooks = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<McpRssKarti />
|
||||
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||
<p className="text-[11px] text-slate-600 leading-relaxed">
|
||||
<strong>Kullanım:</strong> İsteklerinize <code className="bg-white px-1 rounded border">Authorization: Bearer ih_live_…</code>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CalendarClock, Eye, Loader2 } from 'lucide-react';
|
||||
import { getToken } from '../../services/auth';
|
||||
|
||||
/**
|
||||
* Haftalık brief tercihi (Pazartesi 08:00): bu hafta yapılacak takipli
|
||||
* ihaleler + geçen hafta sonuçlanan takipliler — Telegram ve e-posta.
|
||||
* "Önizle" şu anki içeriği gösterir; anahtar kapatır.
|
||||
*/
|
||||
const iste = (opts = {}) => fetch('/api/me/brief', { headers: { Authorization: `Bearer ${getToken()}`, 'Content-Type': 'application/json' }, ...opts }).then((r) => r.json());
|
||||
|
||||
export const BriefTercihi = () => {
|
||||
const [kapali, setKapali] = useState(false);
|
||||
const [metin, setMetin] = useState(null);
|
||||
const [mesgul, setMesgul] = useState(false);
|
||||
|
||||
useEffect(() => { iste().then((d) => { if (d?.success) setKapali(!!d.kapali); }).catch(() => {}); }, []);
|
||||
|
||||
const degistir = async () => {
|
||||
const yeni = !kapali; setKapali(yeni);
|
||||
try { await iste({ method: 'POST', body: JSON.stringify({ kapali: yeni }) }); } catch { setKapali(!yeni); }
|
||||
};
|
||||
const onizle = async () => {
|
||||
setMesgul(true);
|
||||
try { const d = await iste(); setMetin(d?.metin || 'Önizleme alınamadı.'); } catch { setMetin('Önizleme alınamadı.'); } finally { setMesgul(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2"><CalendarClock className="w-4 h-4 text-amber-500" /> Haftalık Brief</h3>
|
||||
<p className="text-[11px] text-slate-500 mt-1">Her Pazartesi 08:00: bu hafta yapılacak takipli ihaleler ve geçen hafta sonuçlanan takipliler — Telegram (bağlıysa) ve e-posta ile tek mesaj.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={onizle} disabled={mesgul} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">
|
||||
{mesgul ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />} Önizle
|
||||
</button>
|
||||
<button role="switch" aria-checked={!kapali} onClick={degistir}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition ${kapali ? 'bg-slate-300' : 'bg-emerald-500'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${kapali ? 'translate-x-0.5' : 'translate-x-4'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{metin && <pre className="whitespace-pre-wrap rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700 font-sans leading-relaxed">{metin}</pre>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plug, Rss, Copy, Check } from 'lucide-react';
|
||||
|
||||
/** MCP sunucusu + RSS besleme kartı (API & Webhooks sekmesi). */
|
||||
export const McpRssKarti = () => {
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : 'https://ihaleist.com';
|
||||
const url = `${origin}/mcp`;
|
||||
const anahtar = 'ih_live_ANAHTARINIZ';
|
||||
const [kopya, setKopya] = useState(null);
|
||||
const kopyala = async (k, t) => { try { await navigator.clipboard.writeText(t); setKopya(k); setTimeout(() => setKopya(null), 1500); } catch { /* yok */ } };
|
||||
const ornekler = [
|
||||
{ k: 'cc', ad: 'Claude Code', not: 'terminalde tek komut', kod: `claude mcp add --transport http ihaleist ${url} --header "Authorization: Bearer ${anahtar}"` },
|
||||
{ k: 'cursor', ad: 'Cursor / Windsurf', not: '.cursor/mcp.json', kod: JSON.stringify({ mcpServers: { ihaleist: { url, headers: { Authorization: `Bearer ${anahtar}` } } } }, null, 2) },
|
||||
{ k: 'desktop', ad: 'Claude Desktop', not: 'claude_desktop_config.json (mcp-remote)', kod: JSON.stringify({ mcpServers: { ihaleist: { command: 'npx', args: ['-y', 'mcp-remote', url, '--header', `Authorization: Bearer ${anahtar}`] } } }, null, 2) },
|
||||
];
|
||||
const rss = `${origin}/api/feeds/tenders.rss?key=${anahtar}&il=İstanbul&q=eldiven`;
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-5 space-y-4">
|
||||
<div className="border-b border-slate-100 pb-3">
|
||||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2"><Plug className="w-4 h-4 text-violet-600" /> MCP Sunucusu — yapay zekâ araçlarınıza İhaleist'i bağlayın</h3>
|
||||
<p className="text-[11px] text-slate-500 mt-1">Claude Code, Cursor, Claude Desktop gibi MCP destekli araçlar İhaleist'i doğrudan sorgular: <code>search_tenders</code>, <code>get_tender</code>, <code>search_results</code>, <code>search_items</code>, <code>upcoming_deadlines</code>. Yukarıdaki API anahtarınızı kullanır.</p>
|
||||
</div>
|
||||
{ornekler.map((o) => (
|
||||
<div key={o.k} className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-bold text-slate-800">{o.ad} <span className="font-normal text-slate-500">— {o.not}</span></p>
|
||||
<button onClick={() => kopyala(o.k, o.kod)} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">{kopya === o.k ? <Check className="w-3 h-3 text-emerald-600" /> : <Copy className="w-3 h-3" />} Kopyala</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded-lg bg-slate-900 text-slate-100 p-2 text-[11px]">{o.kod}</pre>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t border-slate-100 pt-3 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-bold text-slate-800 flex items-center gap-1"><Rss className="w-3.5 h-3.5 text-orange-500" /> RSS beslemesi <span className="font-normal text-slate-500">— Feedly, Outlook, Slack RSS, n8n… (il, sektor, q filtreleri)</span></p>
|
||||
<button onClick={() => kopyala('rss', rss)} className="text-[11px] flex items-center gap-1 border border-slate-200 rounded-lg px-2 py-1 hover:bg-slate-50">{kopya === 'rss' ? <Check className="w-3 h-3 text-emerald-600" /> : <Copy className="w-3 h-3" />} Kopyala</button>
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded-lg bg-slate-900 text-slate-100 p-2 text-[11px]">{rss}</pre>
|
||||
<p className="text-[11px] text-slate-500">Sonuç ilanları beslemesi anahtarsız: <code>{origin}/api/feeds/sonuc.rss</code> (firma, idare, il filtreleri). Anahtarı <code>ih_live_ANAHTARINIZ</code> yerine yazın.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { TelegramBaglanti } from './TelegramBaglanti';
|
||||
import { BriefTercihi } from './BriefTercihi';
|
||||
import { getToken } from '../../services/auth';
|
||||
|
||||
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
@@ -108,7 +109,8 @@ export const NotifySubscriptions = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TelegramBaglanti />
|
||||
<BriefTercihi />
|
||||
<TelegramBaglanti />
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||||
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { redFlags, marketConcentration, formatTL } from '../../services/analytics';
|
||||
import { ShieldAlert, AlertTriangle, TrendingDown, Building2, Users, Info } from 'lucide-react';
|
||||
import { ShieldAlert, AlertTriangle, TrendingDown, Building2, Users, Info, Clock, CalendarX, Hourglass, EyeOff, Scissors } from 'lucide-react';
|
||||
|
||||
const SEVIYE = {
|
||||
yuksek: { etiket: 'Yüksek', kutu: 'bg-red-50 border-red-200', rozet: 'bg-red-100 text-red-800', cizgi: 'bg-red-500' },
|
||||
@@ -15,6 +15,11 @@ const TUR_IKON = {
|
||||
'Tahmin üstü bedel': AlertTriangle,
|
||||
'Aşırı düşük teklif': TrendingDown,
|
||||
'Tedarikçi yoğunlaşması': Building2,
|
||||
'Kısa süre': Clock,
|
||||
'Hafta sonu tarih': CalendarX,
|
||||
'Geç sözleşme': Hourglass,
|
||||
'Kazananı gizli': EyeOff,
|
||||
'Parçalama şüphesi': Scissors,
|
||||
};
|
||||
|
||||
export const RedFlags = () => {
|
||||
|
||||
Reference in New Issue
Block a user