Files
ihaleist/src/components/TenderCard.jsx
T

241 lines
12 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 from 'react';
import { useApp } from '../context/AppContext';
import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles, CalendarDays, Megaphone } from 'lucide-react';
export const TenderCard = ({ tender }) => {
const { toggleTrackTender, setActiveTenderDetail, favoriteKurumlar, favoriteSaving, toggleFavoriteKurum, kalemEslesen, searchQuery } = useApp();
// Aramada eşleşen kalemler öne alınır — firma kendi ürününü hemen görsün.
const eslesenKalemler = (kalemEslesen && tender.ikn && kalemEslesen.get(tender.ikn)) || null;
const isFavKurum = favoriteKurumlar?.includes((tender.kurum || '').trim());
const parsedDateMatch = /^(\d{2})\.(\d{2})\.(\d{4})(?:\s|$)/.exec(String(tender.tarih || '').trim());
const dateMatch = parsedDateMatch && (() => {
const date = new Date(Number(parsedDateMatch[3]), Number(parsedDateMatch[2]) - 1, Number(parsedDateMatch[1]));
return date.getFullYear() === Number(parsedDateMatch[3]) && date.getMonth() === Number(parsedDateMatch[2]) - 1 && date.getDate() === Number(parsedDateMatch[1]) ? parsedDateMatch : null;
})();
const day = dateMatch?.[1] || '—';
const monthYearFormatted = dateMatch ? `${dateMatch[2]}.${dateMatch[3]}` : 'Tarih doğrulanmalı';
// İlan (yayım) tarihi kaynaklara göre farklı biçimde geliyor:
// canlı akış ISO (2026-08-30), DT/arşiv gg.aa.yyyy. İkisini de gösterebilelim.
const ilanTarihiGoster = (() => {
const v = tender.ilanTarihi;
if (!v) return null;
if (/^\d{4}-\d{2}-\d{2}/.test(v)) {
const [y, a, g] = v.slice(0, 10).split('-');
return `${g}.${a}.${y}`;
}
return v;
})();
return (
<div className="bg-white rounded-xl border border-blue-200/80 shadow-xs hover:shadow-md transition-all overflow-hidden flex flex-col md:flex-row group border-l-4 border-l-blue-600">
{/* Left Date Block */}
<button type="button"
onClick={() => setActiveTenderDetail(tender)}
aria-label={`${tender.tarih || 'Tarih doğrulanmalı'} tarih bilgisi; ihale detayını aç`}
className="bg-[#edf4ff] group-hover:bg-[#e2edff] text-blue-900 md:w-36 p-4 flex flex-col items-center justify-center shrink-0 border-b md:border-b-0 md:border-r border-blue-100 cursor-pointer select-none transition-colors text-center focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-[-2px]"
>
<span className="text-[9px] font-black uppercase tracking-widest text-blue-900/50">İhale</span>
<span className="text-3xl font-black tracking-tight text-blue-700 leading-none">{day}</span>
<span className="text-xs font-bold text-blue-900/80 mt-0.5">{monthYearFormatted}</span>
<span className="text-[11px] font-semibold bg-white/80 px-2 py-0.5 rounded-full text-blue-800 mt-2 border border-blue-200">
{tender.saat || 'Saat doğrulanmalı'}
</span>
</button>
{/* Main Card Content */}
<div className="flex-1 p-4 flex flex-col justify-between space-y-3">
{/* Top Header Row */}
<div>
<div className="flex items-start justify-between gap-3">
<h3><button type="button" aria-haspopup="dialog"
onClick={() => setActiveTenderDetail(tender)}
className="text-left focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 rounded font-bold text-sm sm:text-base text-slate-900 hover:text-blue-600 cursor-pointer leading-snug tracking-tight"
>
{tender.baslik}
{tender.isYeni && (
<span className="ml-2 inline-block bg-purple-600 text-white text-[10px] font-extrabold px-2 py-0.5 rounded-md uppercase tracking-wider">
YENİ
</span>
)}
</button></h3>
{/* Bookmark Star Button */}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleTrackTender(tender.id);
}}
title={tender.takipte ? 'Takibi Bırak' : 'Takibe Al'}
className={`p-1.5 rounded-lg transition-transform active:scale-95 ${
tender.takipte ? 'text-amber-400 bg-amber-50' : 'text-slate-300 hover:text-amber-400 hover:bg-slate-50'
}`}
>
<Star className={`w-5 h-5 ${tender.takipte ? 'fill-amber-400' : ''}`} />
</button>
</div>
{/* Subtitle Details */}
<div className="flex flex-wrap items-center gap-y-1 gap-x-3 text-xs text-slate-500 mt-1.5 font-medium">
<span className="flex items-center gap-1 text-slate-700 font-semibold truncate max-w-md">
<Building2 className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<span className="truncate">{tender.kurum}</span>
<button
onClick={(e) => { e.stopPropagation(); toggleFavoriteKurum(tender.kurum); }}
disabled={favoriteSaving || !tender.kurum?.trim()}
aria-busy={favoriteSaving} aria-pressed={Boolean(isFavKurum)}
aria-label={`${tender.kurum} — ${isFavKurum ? 'Favori kurumdan çıkar' : 'Kurumu favorilere ekle'}`}
title={isFavKurum ? 'Favori kurumdan çıkar' : 'Kurumu favorilere ekle'}
className="shrink-0 p-0.5"
>
<Star className={`w-3.5 h-3.5 ${isFavKurum ? 'text-amber-500 fill-amber-500' : 'text-slate-300 hover:text-amber-400'}`} />
</button>
</span>
<span className="flex items-center gap-1 text-slate-600">
<MapPin className="w-3.5 h-3.5 text-rose-500 shrink-0" />
<span>{tender.sehir}</span>
</span>
{ilanTarihiGoster && (
<span
className="bg-emerald-50 text-emerald-700 border border-emerald-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1"
title="İlanın yayımlandığı tarih"
>
<Megaphone className="w-3 h-3 text-emerald-600" />
<span>İlan: {ilanTarihiGoster}</span>
</span>
)}
{tender.tarih && (
<span
className="bg-blue-50 text-blue-700 border border-blue-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1"
title="İhalenin yapılacağı tarih"
>
<CalendarDays className="w-3 h-3 text-blue-600" />
<span>İhale: {tender.tarih}{tender.saat ? ` ${tender.saat}` : ''}</span>
</span>
)}
{tender.kalemSayisi > 0 && (
<span
className="bg-indigo-50 text-indigo-700 border border-indigo-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1"
title="Teklif cetveli kalemleri — bir kullanıcının yüklediği ihale dokümanından çıkarıldı"
>
<Package className="w-3 h-3 text-indigo-600" />
<span>{tender.kalemSayisi} kalem</span>
</span>
)}
{tender.eklenmeZamani && !ilanTarihiGoster && (
<span className="bg-slate-100 text-slate-600 border border-slate-200 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1">
<Clock className="w-3 h-3 text-slate-500" />
<span>{tender.eklenmeZamani}</span>
</span>
)}
<span className="text-slate-400 font-mono text-[11px]">
# {tender.ikn}
</span>
</div>
</div>
{/* Badges Row */}
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span className="bg-amber-500 text-white font-extrabold text-[11px] px-2.5 py-1 rounded-md shadow-2xs">
{tender.alimTuru}
</span>
<span className="bg-slate-100 text-slate-700 font-medium text-[11px] px-2.5 py-1 rounded-md border border-slate-200">
{tender.sektor}
</span>
{tender.cpvLabel && tender.cpvLabel !== 'Genel' && tender.cpvLabel !== tender.sektor && (
<span className="bg-sky-50 text-sky-700 font-medium text-[11px] px-2.5 py-1 rounded-md border border-sky-200">
{tender.cpvLabel}
</span>
)}
{tender.aciliyetBadge && (
<span className="bg-red-500 text-white font-extrabold text-[11px] px-2 py-1 rounded-md flex items-center gap-1 shadow-2xs">
<AlertTriangle className="w-3 h-3" />
<span>{tender.aciliyetBadge}</span>
</span>
)}
{tender.aiMatchScore != null && (
<span
title={tender.uyumNedenleri?.length
? tender.uyumNedenleri.map((n) => `‘${n.terim}’ ${n.alan}nde geçiyor`).join('\n')
: 'Profilinizdeki terimlerden hiçbiri bu ihalede geçmiyor'}
className="bg-blue-50 text-blue-700 border border-blue-200 text-[11px] font-bold px-2 py-1 rounded-md flex items-center gap-1">
<Sparkles className="w-3 h-3 text-blue-600" />
<span>%{tender.aiMatchScore} Uyumlu</span>
</span>
)}
{tender.kaynak && (
<span className="bg-emerald-50 text-emerald-700 border border-emerald-200 text-[11px] font-bold px-2 py-1 rounded-md">
🟢 {tender.kaynak}
</span>
)}
</div>
{/* Teklif cetveli kalemleri (ortak havuz) — firmanın kendi ürününü
şartnameyi indirmeden görebilmesi için kartta gösterilir. */}
{tender.kalemSayisi > 0 && (
<div
onClick={() => setActiveTenderDetail(tender)}
className="bg-indigo-50/60 hover:bg-indigo-50 rounded-lg p-2.5 border border-indigo-200 space-y-1.5 cursor-pointer transition-colors"
>
<p className="text-[10px] font-black uppercase tracking-wider text-indigo-700/70">
{eslesenKalemler ? 'Aramanızla eşleşen kalemler' : 'Teklif cetveli kalemleri'}
</p>
{(eslesenKalemler
? eslesenKalemler.slice(0, 3).map((k) => k)
: (tender.kalemler || []).slice(0, 3).map((ad) => ({ ad }))
).map((k, idx) => (
<div key={idx} className="flex items-center justify-between gap-2 text-xs font-semibold text-slate-700">
<span className="flex items-center gap-2 truncate">
<Package className="w-3.5 h-3.5 text-indigo-500 shrink-0" />
<span className="truncate">{k.ad}</span>
</span>
{k.miktar != null && (
<span className="bg-white border border-indigo-200 px-2 py-0.5 rounded text-[11px] font-bold text-slate-900 shrink-0">
{k.miktar.toLocaleString('tr-TR')} {k.birim || ''}
</span>
)}
</div>
))}
{tender.kalemSayisi > 3 && (
<p className="text-[10px] font-bold text-indigo-600 hover:underline pt-0.5 text-right">
+ {tender.kalemSayisi - 3} kalem daha — tümünü gör
</p>
)}
</div>
)}
{/* Material Items Preview Box */}
{!tender.kalemSayisi && tender.items && tender.items.length > 0 && (
<div
onClick={() => setActiveTenderDetail(tender)}
className="bg-slate-50 hover:bg-slate-100/80 rounded-lg p-2.5 border border-slate-200 space-y-1.5 cursor-pointer transition-colors"
>
{tender.items.slice(0, 3).map((item, idx) => (
<div key={idx} className="flex items-center justify-between text-xs font-semibold text-slate-700">
<span className="flex items-center gap-2 truncate">
<Package className="w-3.5 h-3.5 text-blue-500 shrink-0" />
<span className="truncate">{item.ad}</span>
</span>
<span className="bg-white border border-slate-200 px-2 py-0.5 rounded text-[11px] font-bold text-slate-900 shrink-0">
{item.miktar} {item.birim || 'adet'}
</span>
</div>
))}
{tender.items.length > 3 && (
<p className="text-[10px] font-bold text-blue-600 hover:underline pt-0.5 text-right">
+ {tender.items.length - 3} malzeme kalemi daha...
</p>
)}
</div>
)}
</div>
</div>
);
};