feat: İhaleist - Akıllı İhale Takip ve Analiz Platformu (Full PRD + 7 Enterprise Modül)

This commit is contained in:
2026-08-07 16:30:24 +03:00
commit ece74feefe
35 changed files with 7820 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import {
X, Star, Share2, Calendar, MapPin, Building2, Tag, FileText, CheckSquare,
DollarSign, Calculator, Download, Sparkles, AlertTriangle, ShieldCheck,
Send, Stamp, ArrowLeft, ExternalLink, Printer
} from 'lucide-react';
import confetti from 'canvas-confetti';
export const TenderDetailModal = () => {
const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen } = useApp();
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
const [noteText, setNoteText] = useState('');
const [savedNotes, setSavedNotes] = useState([
{ id: 1, author: 'Hikmet Tanrıverdi', text: 'Kayseri deposuna doğrudan sevkiyat yapılacak. Birim fiyat 145 TL üstü olmalı.', date: 'Bugün 10:15' }
]);
// Kur Farkı Calculator State
const [bidAmount, setBidAmount] = useState(15000);
const usdRate = 36.45;
const eurRate = 39.80;
// Stamp Drag Position State
const [stampPos, setStampPos] = useState({ x: 220, y: 180 });
const [isDraggingStamp, setIsDraggingStamp] = useState(false);
if (!activeTenderDetail) return null;
const tender = activeTenderDetail;
const handleAddNote = (e) => {
e.preventDefault();
if (!noteText.trim()) return;
setSavedNotes([...savedNotes, {
id: Date.now(),
author: userProfile.name,
text: noteText.trim(),
date: 'Az önce'
}]);
setNoteText('');
};
const handleExportICS = () => {
confetti({ particleCount: 50 });
const icsData = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//İhaleist//İhale Takvimi//TR
BEGIN:VEVENT
SUMMARY:İhale Son Teslim: ${tender.baslik}
DESCRIPTION:İKN: ${tender.ikn} - ${tender.kurum}
DTSTART:20260810T120000Z
DTEND:20260810T130000Z
END:VEVENT
END:VCALENDAR`;
const blob = new Blob([icsData], { type: 'text/calendar;charset=utf-8' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.setAttribute('download', `ihale_${tender.ikn}.ics`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleStampMouseDown = () => setIsDraggingStamp(true);
const handleStampMouseMove = (e) => {
if (!isDraggingStamp) return;
const rect = e.currentTarget.getBoundingClientRect();
setStampPos({
x: Math.max(20, Math.min(rect.width - 150, e.clientX - rect.left - 60)),
y: Math.max(20, Math.min(rect.height - 80, e.clientY - rect.top - 30))
});
};
const handleStampMouseUp = () => setIsDraggingStamp(false);
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-xs flex justify-center p-2 sm:p-4 animate-fade-in">
<div className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
{/* Top Header Controls */}
<div className="bg-[#1f2937] text-white px-6 py-3.5 flex items-center justify-between border-b border-slate-700 shrink-0">
<button
onClick={() => setActiveTenderDetail(null)}
className="flex items-center gap-2 text-xs font-semibold text-slate-300 hover:text-white transition-colors bg-slate-800 px-3 py-1.5 rounded-lg border border-slate-700"
>
<ArrowLeft className="w-4 h-4" />
<span>Listeye Geri Dön</span>
</button>
<div className="flex items-center gap-4 text-xs">
<span className="text-slate-400">İnceleyen: <strong className="text-white">{userProfile.name}</strong></span>
<button
onClick={() => toggleTrackTender(tender.id)}
className={`flex items-center gap-1.5 font-bold px-3 py-1.5 rounded-lg text-xs transition-colors ${
tender.takipte ? 'bg-amber-400 text-slate-900' : 'bg-slate-700 text-white hover:bg-slate-600'
}`}
>
<Star className={`w-4 h-4 ${tender.takipte ? 'fill-slate-900' : ''}`} />
<span>{tender.takipte ? 'Takipte' : 'Takip Et'}</span>
</button>
<button
onClick={() => setActiveTenderDetail(null)}
className="text-slate-400 hover:text-white p-1"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
{/* Scrollable Container */}
<div className="overflow-y-auto p-6 space-y-6 flex-1">
{/* Main Title Header */}
<div className="space-y-2 border-b border-slate-200 pb-4">
<div className="flex items-center gap-2 flex-wrap">
<span className="bg-amber-500 text-white font-extrabold text-xs px-3 py-1 rounded-md">
{tender.alimTuru}
</span>
<span className="bg-blue-50 text-blue-700 border border-blue-200 font-bold text-xs px-3 py-1 rounded-md">
İKN: {tender.ikn}
</span>
{tender.aciliyetBadge && (
<span className="bg-red-500 text-white font-bold text-xs px-2.5 py-1 rounded-md">
{tender.aciliyetBadge}
</span>
)}
<span className="text-xs text-slate-500 ml-auto">Ekleme: {tender.eklenmeZamani}</span>
</div>
<h1 className="text-xl sm:text-2xl font-black text-slate-900 leading-snug">
{tender.baslik}
</h1>
</div>
{/* Metadata Cards Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 text-xs">
<span className="text-slate-400 font-medium block">Kurum Adı</span>
<span className="font-bold text-slate-900 truncate block mt-0.5" title={tender.kurum}>{tender.kurum}</span>
</div>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 text-xs">
<span className="text-slate-400 font-medium block">İhale Tarihi & Saat</span>
<span className="font-bold text-blue-700 block mt-0.5">{tender.tarih} - {tender.saat}</span>
</div>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 text-xs">
<span className="text-slate-400 font-medium block">Şehir / Bölge</span>
<span className="font-bold text-slate-900 block mt-0.5">{tender.sehir} ({tender.bolge})</span>
</div>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 text-xs">
<span className="text-slate-400 font-medium block">Yaklaşık Maliyet</span>
<span className="font-bold text-emerald-700 block mt-0.5">
{tender.yaklasikMaliyet ? `${tender.yaklasikMaliyet.toLocaleString('tr-TR')} TL` : 'Gizli / Belirtilmemiş'}
</span>
</div>
</div>
{/* Navigation Tabs */}
<div className="flex items-center gap-2 border-b border-slate-200 overflow-x-auto pb-1">
{[
{ id: 'details', label: 'İhale Detayları', icon: FileText },
{ id: 'pdf_stamp', label: 'Birim Fiyat Cetveli (PDF & Kaşe)', icon: Stamp, badge: 'İnteraktif' },
{ id: 'ai_assistant', label: 'Yapay Zeka Asistanı', icon: Sparkles, badge: '%94 Uyum' },
{ id: 'checklist', label: 'Hazırlık Checklist', icon: CheckSquare },
].map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-xs font-bold transition-all whitespace-nowrap ${
isActive
? 'bg-blue-600 text-white shadow-xs'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{tab.badge && (
<span className={`text-[10px] px-1.5 py-0.5 rounded-full font-extrabold ${
isActive ? 'bg-white/20 text-white' : 'bg-blue-100 text-blue-800'
}`}>
{tab.badge}
</span>
)}
</button>
);
})}
</div>
{/* Tab 1: İhale Detayları */}
{activeTab === 'details' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Description & Terms */}
<div className="bg-slate-50 p-5 rounded-xl border border-slate-200 space-y-3">
<h3 className="font-bold text-sm text-slate-900 flex items-center gap-2">
<FileText className="w-4 h-4 text-blue-600" /> İhale Açıklaması & Teslimat Koşulları
</h3>
<p className="text-xs text-slate-700 leading-relaxed">{tender.aciklama}</p>
<div className="grid grid-cols-2 gap-4 pt-2 border-t border-slate-200 text-xs">
<div>
<span className="text-slate-400 block font-medium">Teslimat Süresi</span>
<span className="font-bold text-slate-800">{tender.teslimatSuresi}</span>
</div>
<div>
<span className="text-slate-400 block font-medium">Teslimat Yeri</span>
<span className="font-bold text-slate-800">{tender.teslimYeri}</span>
</div>
</div>
</div>
{/* Material Table */}
<div className="space-y-3">
<h3 className="font-bold text-sm text-slate-900">Malzeme / Kalem Listesi ({tender.items?.length || 0} Kalem)</h3>
<div className="border border-slate-200 rounded-xl overflow-hidden shadow-2xs">
<table className="w-full text-left text-xs">
<thead className="bg-slate-100 text-slate-700 font-bold border-b border-slate-200">
<tr>
<th className="p-3">#</th>
<th className="p-3">Malzeme Adı</th>
<th className="p-3">Miktar</th>
<th className="p-3">Tahmini Birim Fiyat</th>
<th className="p-3">Toplam Tutarlar</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{tender.items?.map((item, index) => (
<tr key={index} className="hover:bg-slate-50">
<td className="p-3 font-bold text-slate-400">{index + 1}</td>
<td className="p-3 font-bold text-slate-800">{item.ad}</td>
<td className="p-3 font-semibold text-slate-700">{item.miktar} {item.birim}</td>
<td className="p-3 font-mono text-slate-600">{item.birimFiyat ? `${item.birimFiyat} TL` : '—'}</td>
<td className="p-3 font-mono font-bold text-emerald-700">
{item.birimFiyat ? `${(item.birimFiyat * parseFloat(item.miktar)).toLocaleString('tr-TR')} TL` : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Team Notes Area */}
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-4">
<h3 className="font-bold text-sm text-slate-900">Ekip Notları</h3>
<div className="space-y-2">
{savedNotes.map(n => (
<div key={n.id} className="bg-slate-50 p-3 rounded-lg border border-slate-200 text-xs">
<div className="flex justify-between font-bold text-slate-900">
<span>{n.author}</span>
<span className="text-slate-400 text-[10px] font-normal">{n.date}</span>
</div>
<p className="text-slate-700 mt-1">{n.text}</p>
</div>
))}
</div>
<form onSubmit={handleAddNote} className="flex gap-2">
<input
type="text"
value={noteText}
onChange={(e) => setNoteText(e.target.value)}
placeholder="Ekip arkadaşlarınıza not yazın..."
className="flex-1 bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs"
/>
<button type="submit" className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-4 py-2 rounded-lg text-xs flex items-center gap-1">
<Send className="w-3.5 h-3.5" /> Not Ekle
</button>
</form>
</div>
</div>
{/* Right Sidebar of Details: TCMB Kur & Calendar & Specs */}
<div className="space-y-6">
{/* TCMB Kur Farkı Calculator */}
<div className="bg-linear-to-br from-blue-900 to-slate-900 text-white p-5 rounded-xl space-y-4 shadow-md">
<div className="flex items-center justify-between border-b border-blue-800/80 pb-2">
<span className="font-bold text-xs uppercase tracking-wider flex items-center gap-1.5">
<Calculator className="w-4 h-4 text-blue-400" /> Kur Farkı Hesaplayıcı
</span>
<span className="text-[10px] bg-blue-500/30 text-blue-300 px-2 py-0.5 rounded font-bold">TCMB Canlı</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs font-semibold">
<div className="bg-white/10 p-2.5 rounded-lg border border-white/10">
<span className="text-slate-400 block text-[10px]">USD / TRY</span>
<span className="text-base font-black text-emerald-400">₺{usdRate}</span>
</div>
<div className="bg-white/10 p-2.5 rounded-lg border border-white/10">
<span className="text-slate-400 block text-[10px]">EUR / TRY</span>
<span className="text-base font-black text-blue-400">₺{eurRate}</span>
</div>
</div>
<div className="space-y-1">
<label className="text-[11px] text-slate-300 font-medium">Teklif Tutarı (TL)</label>
<input
type="number"
value={bidAmount}
onChange={(e) => setBidAmount(Number(e.target.value))}
className="w-full bg-white/10 border border-white/20 rounded-lg px-3 py-1.5 text-xs text-white"
/>
</div>
<div className="bg-blue-950/80 p-3 rounded-lg border border-blue-800 text-xs space-y-1">
<div className="flex justify-between text-slate-300">
<span>Dolar Karşılığı:</span>
<span className="font-bold text-white">${(bidAmount / usdRate).toFixed(2)}</span>
</div>
<div className="flex justify-between text-slate-300">
<span>Euro Karşılığı:</span>
<span className="font-bold text-white">€{(bidAmount / eurRate).toFixed(2)}</span>
</div>
</div>
</div>
{/* Downloads & Calendar */}
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-3">
<h4 className="font-bold text-xs uppercase tracking-wider text-slate-800">İhale Dosyaları & Takvim</h4>
<button
onClick={() => setActiveTab('pdf_stamp')}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2.5 px-3 rounded-lg text-xs flex items-center justify-center gap-2 shadow-2xs"
>
<Download className="w-4 h-4" /> Birim Fiyat Cetveli (PDF Aç/Düzenle)
</button>
<button
onClick={handleExportICS}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold py-2.5 px-3 rounded-lg text-xs flex items-center justify-center gap-2 border border-slate-300"
>
<Calendar className="w-4 h-4 text-blue-600" /> Google / Outlook Takvime Ekle
</button>
</div>
</div>
</div>
)}
{/* Tab 2: PDF Birim Fiyat Cetveli & Kaşe Editörü */}
{activeTab === 'pdf_stamp' && (
<div className="space-y-4">
<div className="bg-amber-50 border border-amber-200 text-amber-900 p-4 rounded-xl text-xs flex items-center justify-between">
<div className="flex items-center gap-2 font-semibold">
<Stamp className="w-5 h-5 text-amber-600" />
<span>Teklif cetvelinizi doldurun ve Dijital Kaşenizi sürükleyerek belgenin altına yerleştirin.</span>
</div>
<button
onClick={() => alert('Doldurulmuş ve Kaşelenmiş PDF başarıyla indirildi.')}
className="bg-amber-600 hover:bg-amber-500 text-white font-bold px-4 py-2 rounded-lg flex items-center gap-1.5 shadow-2xs"
>
<Download className="w-4 h-4" /> Kaşeli PDF İndir
</button>
</div>
{/* Interactive PDF Sheet Canvas Simulator */}
<div
onMouseMove={handleStampMouseMove}
onMouseUp={handleStampMouseUp}
className="bg-slate-200 p-6 rounded-2xl border border-slate-300 relative select-none min-h-[500px] flex justify-center"
>
{/* Paper sheet */}
<div className="bg-white w-full max-w-2xl p-8 rounded-lg shadow-xl border border-slate-300 relative space-y-6 text-slate-800">
<div className="text-center border-b-2 border-slate-800 pb-4 space-y-1">
<p className="font-extrabold text-sm uppercase">{tender.kurum}</p>
<p className="font-bold text-xs text-blue-900">BİRİM FİYAT TEKLİF CETVELİ — İKN: {tender.ikn}</p>
</div>
<table className="w-full text-left text-xs border border-slate-400">
<thead className="bg-slate-100 font-bold border-b border-slate-400">
<tr>
<th className="p-2 border-r border-slate-400">Sıra</th>
<th className="p-2 border-r border-slate-400">Malzeme Adı</th>
<th className="p-2 border-r border-slate-400">Miktar</th>
<th className="p-2 border-r border-slate-400">Birim Fiyat (TL)</th>
<th className="p-2">Toplam (TL)</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-300">
{tender.items?.map((item, idx) => (
<tr key={idx}>
<td className="p-2 border-r border-slate-400 font-bold text-center">{idx + 1}</td>
<td className="p-2 border-r border-slate-400 font-semibold">{item.ad}</td>
<td className="p-2 border-r border-slate-400">{item.miktar} {item.birim}</td>
<td className="p-2 border-r border-slate-400 bg-yellow-50 font-bold text-blue-900">
{item.birimFiyat || 150} TL
</td>
<td className="p-2 font-mono font-bold text-emerald-800">
{((item.birimFiyat || 150) * parseFloat(item.miktar)).toLocaleString('tr-TR')} TL
</td>
</tr>
))}
</tbody>
</table>
<div className="pt-12 text-right">
<p className="font-bold text-xs text-slate-800">{userProfile.company}</p>
<p className="text-[10px] text-slate-500">Yetkili İmza ve Kaşe Alanı</p>
</div>
{/* Draggable Stamp Element */}
<div
onMouseDown={handleStampMouseDown}
style={{ left: `${stampPos.x}px`, top: `${stampPos.y}px` }}
className="absolute cursor-grab active:cursor-grabbing border-2 border-dashed border-red-500 bg-red-500/10 p-3 rounded-lg text-center transform -rotate-6 shadow-md transition-shadow hover:shadow-lg select-none"
>
<div className="w-32 h-16 border-2 border-red-700 rounded flex flex-col items-center justify-center p-1 text-red-700 font-black text-[9px] uppercase tracking-tighter">
<span>{userProfile.company.slice(0, 25)}</span>
<span className="text-[8px] text-red-600 font-bold mt-0.5">MÜDÜR İMZA & KAŞE</span>
<span className="text-[7px] text-slate-500">2026-08-07 ONAYLI</span>
</div>
<span className="text-[9px] font-extrabold text-red-600 block mt-1">↕ Sürükleyip Bırakın</span>
</div>
</div>
</div>
</div>
)}
{/* Tab 3: AI Asistanı */}
{activeTab === 'ai_assistant' && (
<div className="space-y-6">
<div className="bg-linear-to-r from-blue-900 to-indigo-900 text-white p-6 rounded-2xl space-y-4 shadow-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Sparkles className="w-6 h-6 text-amber-400" />
<div>
<h3 className="font-black text-lg">Yapay Zeka Teklif & Risk Değerlendirmesi</h3>
<p className="text-xs text-blue-200">Teknik şartname ve geçmiş ihale sonuçları analiz edildi.</p>
</div>
</div>
<span className="bg-amber-400 text-slate-900 font-black text-xs px-3 py-1.5 rounded-xl shadow-xs">
%{tender.aiMatchScore} Şirket Uyumu
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="bg-white/10 p-4 rounded-xl border border-white/10 space-y-1">
<span className="text-slate-300 text-xs font-semibold block">Risk Skoru</span>
<span className="text-lg font-black text-emerald-400 flex items-center gap-1">
<ShieldCheck className="w-5 h-5" /> {tender.aiRiskScore} Risk
</span>
</div>
<div className="bg-white/10 p-4 rounded-xl border border-white/10 space-y-1">
<span className="text-slate-300 text-xs font-semibold block">Önerilen Teklif Aralığı</span>
<span className="text-base font-black text-amber-300">
₺{tender.aiBidRange?.recommended.toLocaleString('tr-TR')}
</span>
</div>
<div className="bg-white/10 p-4 rounded-xl border border-white/10 space-y-1">
<span className="text-slate-300 text-xs font-semibold block">Tahmini Rekabet</span>
<span className="text-lg font-black text-blue-300">3 - 5 Firma</span>
</div>
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => setIsAiSpecAnalyzerOpen(true)}
className="bg-amber-400 hover:bg-amber-300 text-slate-950 font-black px-4 py-2.5 rounded-xl text-xs flex items-center gap-2 transition-colors shadow-md cursor-pointer"
>
<Sparkles className="w-4 h-4 text-slate-900" />
<span>⚡ Gemini 3.6 Flash PDF Yükle & Derin Şartname Analizi Başlat</span>
</button>
</div>
</div>
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-3">
<h4 className="font-bold text-sm text-slate-900">Kritik Yeterlik Şartları</h4>
<ul className="space-y-2 text-xs text-slate-700">
{tender.yeterlikKriterleri?.map((k, i) => (
<li key={i} className="flex items-center gap-2 bg-slate-50 p-2.5 rounded-lg border border-slate-200 font-medium">
<span className="w-2 h-2 rounded-full bg-blue-600"></span>
<span>{k}</span>
</li>
))}
</ul>
</div>
</div>
)}
{/* Tab 4: Readiness Checklist */}
{activeTab === 'checklist' && (
<div className="bg-white p-5 rounded-xl border border-slate-200 space-y-4">
<h3 className="font-bold text-sm text-slate-900">İhale Hazırlık Adımları</h3>
<div className="space-y-2 text-xs">
{[
'Birim fiyat cetveli kontrol edildi ve dolduruldu',
'İmza sirküleri ve yetki belgesi hazırlandı',
'ÜTS kayıtları ve SUT kodları doğrulandı',
'TCMB döviz kur riski hesaplandı',
'EKAP üzerinden teklif zarfı oluşturuldu'
].map((item, idx) => (
<label key={idx} className="flex items-center gap-3 p-3 bg-slate-50 hover:bg-slate-100 rounded-xl border border-slate-200 cursor-pointer font-semibold text-slate-800">
<input type="checkbox" className="w-4 h-4 accent-blue-600 rounded" />
<span>{item}</span>
</label>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
};