Files
ihaleist/src/components/tools/TedTenders.jsx
T
hikmettanandClaude Fable 5.1 c32446db81 AB İhaleleri (TED): başlık ve açıklamalar Türkçe
- server/ted-ceviri.mjs: TED'in kendi Türkçe alanı varsa onu kullanır; yoksa
  OpenRouter ile toplu çevirir (deepseek-v4-flash → gemini 3.7 flash → glm),
  sonucu server/data/ted.sqlite'ta ilan numarasına göre saklar
- İstek en fazla 12 sn bekler, kalan çeviriler arka planda sürer;
  yanıt ceviriBekleniyor bayrağı taşır, ön yüz sessizce yeniden çeker
- Kartta Türkçe başlık + küçük özgün başlık, Türkçe açıklama (3 satır),
  ülke kodları Türkçe ad (TUR → Türkiye, AUT → Avusturya…)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 15:58:28 +03:00

184 lines
8.8 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, { useState, useEffect } from 'react';
import { Globe2, Loader2, ExternalLink, Building2, Calendar, Trophy, AlertTriangle, RefreshCw } from 'lucide-react';
const MODLAR = [
{ id: 'acik', label: 'Açık AB İhaleleri', aciklama: 'Türkiye’deki idarelerin AB’de yayımladığı, hâlen teklif verilebilir ihaleler.' },
{ id: 'turkiye', label: 'Tüm Türkiye Kayıtları', aciklama: 'TR idareler + TR kazananlar + TR teklif verenler.' },
{ id: 'kazanan', label: 'AB’de Kazanan TR Firmaları', aciklama: 'Türk firmalarının AB ihalelerinde kazandığı sözleşmeler.' },
];
const ULKE_TR = {
TUR: 'Türkiye', DEU: 'Almanya', FRA: 'Fransa', ITA: 'İtalya', ESP: 'İspanya', NLD: 'Hollanda', BEL: 'Belçika',
AUT: 'Avusturya', POL: 'Polonya', ROU: 'Romanya', BGR: 'Bulgaristan', GRC: 'Yunanistan', HRV: 'Hırvatistan',
HUN: 'Macaristan', CZE: 'Çekya', SVK: 'Slovakya', SVN: 'Slovenya', PRT: 'Portekiz', SWE: 'İsveç', DNK: 'Danimarka',
FIN: 'Finlandiya', NOR: 'Norveç', IRL: 'İrlanda', LTU: 'Litvanya', LVA: 'Letonya', EST: 'Estonya', CYP: 'Kıbrıs',
MLT: 'Malta', LUX: 'Lüksemburg', CHE: 'İsviçre', GBR: 'İngiltere', SRB: 'Sırbistan', MKD: 'K. Makedonya',
ALB: 'Arnavutluk', BIH: 'Bosna-Hersek', MNE: 'Karadağ', XKX: 'Kosova', UKR: 'Ukrayna', MDA: 'Moldova', ISL: 'İzlanda',
};
export const TedTenders = () => {
const [mod, setMod] = useState('acik');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Çeviriler sunucuda arka planda tamamlanır; "bekleniyor" geldiği sürece
// sessizce (yükleniyor göstergesi olmadan) birkaç kez yeniden çekilir.
const yenidenDeneme = React.useRef(0);
const yukle = (m, sessiz = false) => {
if (!sessiz) { setLoading(true); setError(null); yenidenDeneme.current = 0; }
fetch(`/api/ted?mod=${m}&limit=30`)
.then((r) => r.json())
.then((d) => {
if (!d.success) { if (!sessiz) setError(d.error); return; }
setData(d);
if (d.ceviriBekleniyor && yenidenDeneme.current < 12) {
yenidenDeneme.current += 1;
setTimeout(() => yukle(m, true), 7000);
}
})
.catch((e) => { if (!sessiz) setError(e.message); })
.finally(() => { if (!sessiz) setLoading(false); });
};
useEffect(() => { yukle(mod); }, [mod]);
const aktifMod = MODLAR.find((m) => m.id === mod);
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-sky-900 text-white p-6 rounded-2xl shadow-lg border border-sky-800/40">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-sky-600 flex items-center justify-center shadow-md">
<Globe2 className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">AB İhaleleri — TED</h2>
<p className="text-xs text-sky-200 mt-0.5">
Tenders Electronic Daily (AB resmi ihale yayını) üzerinden Türkiye ile ilgili ihaleler.
</p>
</div>
</div>
</div>
{/* Mod seçici */}
<div className="flex gap-2 flex-wrap">
{MODLAR.map((m) => (
<button
key={m.id}
onClick={() => setMod(m.id)}
className={`px-4 py-2 rounded-xl text-xs font-bold border transition-colors ${
mod === m.id ? 'bg-sky-600 text-white border-sky-600 shadow-xs' : 'bg-white text-slate-700 border-slate-200 hover:bg-slate-50'
}`}
>
{m.label}
</button>
))}
<button
onClick={() => yukle(mod)}
disabled={loading}
className="px-3 py-2 rounded-xl text-xs font-bold border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 flex items-center gap-1.5 disabled:opacity-50"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} /> Yenile
</button>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center justify-between gap-3 flex-wrap shadow-xs">
<p className="text-xs text-slate-600">{aktifMod?.aciklama}</p>
{data && (
<span className="text-xs font-bold text-sky-800 bg-sky-50 border border-sky-200 px-3 py-1 rounded-full shrink-0">
{data.totalCount.toLocaleString('tr-TR')} kayıt
</span>
)}
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex gap-3">
<AlertTriangle className="w-4 h-4 text-red-600 shrink-0 mt-0.5" />
<div>
<p className="text-xs font-bold text-red-900">TED verisi alınamadı</p>
<p className="text-xs text-red-800 mt-0.5">{error}</p>
</div>
</div>
)}
{loading && !data && (
<div className="bg-white rounded-2xl border border-slate-200 p-10 text-center">
<Loader2 className="w-6 h-6 text-sky-600 animate-spin mx-auto" />
<p className="text-xs text-slate-500 mt-3">TED'den çekiliyor…</p>
</div>
)}
{data?.ceviriBekleniyor && (
<p className="text-[11px] text-slate-500 flex items-center gap-1"><Loader2 className="w-3 h-3 animate-spin" /> Başlıklar Türkçeye çevriliyor, birkaç saniye içinde güncellenir…</p>
)}
{data && (
<div className="space-y-3">
{data.tenders.length === 0 && (
<div className="bg-white rounded-2xl border border-dashed border-slate-300 p-10 text-center">
<p className="text-sm font-bold text-slate-500">Bu filtrede kayıt bulunamadı</p>
</div>
)}
{data.tenders.map((t) => (
<div key={t.id} className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs hover:shadow-md transition-shadow space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="font-bold text-sm text-slate-900 leading-snug">{t.baslikTr || t.baslik}</h3>
{t.baslikTr && t.baslikTr !== t.baslik && (
<p className="text-[11px] italic text-slate-400 leading-snug mt-0.5">{t.baslik}</p>
)}
</div>
{t.link && (
<a href={t.link} target="_blank" rel="noopener noreferrer"
className="text-sky-600 hover:text-sky-800 shrink-0" title="TED'de aç">
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
<div className="flex items-center gap-3 flex-wrap text-[11px] text-slate-600">
<span className="flex items-center gap-1 font-semibold">
<Building2 className="w-3 h-3 text-slate-400" /> {t.kurum}
</span>
{t.sehir && <span>{t.sehir}</span>}
{t.ulke && <span className="bg-slate-100 px-1.5 py-0.5 rounded font-bold">{ULKE_TR[t.ulke] || t.ulke}</span>}
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3 text-blue-500" />
{t.sonTeklifTarihi ? `Son teklif: ${t.sonTeklifTarihi}` : t.yayimTarihi}
</span>
<span className="bg-sky-50 text-sky-800 border border-sky-200 px-1.5 py-0.5 rounded font-bold">{t.alimTuru}</span>
{t.sonucIlani && <span className="bg-emerald-50 text-emerald-800 border border-emerald-200 px-1.5 py-0.5 rounded font-bold">Sonuç İlanı</span>}
</div>
{(t.aciklamaTr || t.aciklama) && (
<p className="text-xs text-slate-600 leading-relaxed line-clamp-3">{t.aciklamaTr || t.aciklama}</p>
)}
{(t.kazananFirma || t.yaklasikMaliyet != null) && (
<div className="flex items-center gap-4 flex-wrap pt-2 border-t border-slate-100 text-[11px]">
{t.kazananFirma && (
<span className="flex items-center gap-1 font-bold text-emerald-800">
<Trophy className="w-3 h-3" /> {t.kazananFirma}
{t.kazananUlke && <span className="text-slate-400 font-normal">({ULKE_TR[t.kazananUlke] || t.kazananUlke})</span>}
</span>
)}
{t.yaklasikMaliyet != null && (
<span className="font-black text-indigo-900">
{t.yaklasikMaliyet.toLocaleString('tr-TR')} {t.paraBirimi || ''}
</span>
)}
</div>
)}
</div>
))}
</div>
)}
<p className="text-[11px] text-slate-400 text-center">
Kaynak: Tenders Electronic Daily (ted.europa.eu) — Avrupa Birliği resmi ihale yayını.
</p>
</div>
);
};