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
+46
View File
@@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import { CalendarClock } from 'lucide-react';
import { useApp } from '../context/AppContext';
import { parseDateTR } from '../services/analytics';
/**
* Sağ panel: takipteki / panodaki ihalelerden 14 gün içinde yapılacaklar.
* Sunucuya gitmez — liste zaten bellekte. ≤3 gün kırmızı, ≤7 turuncu.
*/
export const YaklasanSonTarihler = ({ gun = 14 }) => {
const { tenders, setActiveTenderDetail } = useApp();
const liste = useMemo(() => {
const bugun = new Date(); bugun.setHours(0, 0, 0, 0);
const sinir = new Date(bugun.getTime() + gun * 86400000);
return tenders
.filter((t) => t.takipte || t.kanbanStatus || t.gorev)
.map((t) => ({ t, d: parseDateTR(t.tarih) }))
.filter(({ d }) => d && d >= bugun && d <= sinir)
.sort((a, b) => a.d - b.d)
.map(({ t, d }) => ({ t, kalan: Math.round((d - bugun) / 86400000) }))
.slice(0, 8);
}, [tenders, gun]);
const renk = (g) => (g <= 3 ? 'bg-red-600 text-white' : g <= 7 ? 'bg-amber-500 text-white' : 'bg-slate-200 text-slate-800');
return (
<div className="bg-white rounded-xl border border-slate-200 shadow-xs overflow-hidden">
<div className="bg-slate-50 border-b border-slate-100 px-4 py-3">
<span className="flex items-center gap-2 font-bold text-xs uppercase tracking-wider text-slate-800">
<CalendarClock className="w-4 h-4 text-amber-500" /> Yaklaşan İhale Tarihleri
</span>
</div>
<div className="p-2 space-y-1">
{liste.length === 0 ? (
<p className="px-2 py-3 text-xs text-slate-500">{gun} gün içinde yapılacak takipli ihale yok. Yıldızla takibe alın; burada geri sayım başlar.</p>
) : liste.map(({ t, kalan }) => (
<button key={t.id} type="button" onClick={() => setActiveTenderDetail?.(t)}
className="w-full flex items-center justify-between gap-2 rounded-lg p-2 text-xs hover:bg-slate-50 text-left">
<span className="min-w-0 truncate font-medium text-slate-800" title={t.baslik}>{t.baslik}</span>
<span className={`shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-extrabold ${renk(kalan)}`}>{kalan === 0 ? 'BUGÜN' : `${kalan} gün`}</span>
</button>
))}
</div>
</div>
);
};