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
+289
View File
@@ -0,0 +1,289 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { Sparkles, Upload, FileText, CheckCircle2, AlertTriangle, ShieldCheck, DollarSign, X, Loader2, ArrowRight, TrendingUp, Sliders } from 'lucide-react';
import confetti from 'canvas-confetti';
export const AiSpecAnalyzerModal = () => {
const { isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen, analyzeSpecPdfWithGemini, aiAnalysisResults } = useApp();
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const [customBidSlider, setCustomBidSlider] = useState(148500);
if (!isAiSpecAnalyzerOpen) return null;
const handleUploadAndAnalyze = async (fileName = 'Teknik_Sartname_Kayseri_Cezaevi.pdf') => {
setIsAnalyzing(true);
setSelectedFile(fileName);
await analyzeSpecPdfWithGemini(fileName);
setIsAnalyzing(false);
confetti({ particleCount: 80, spread: 70 });
};
const results = aiAnalysisResults || {
fileName: 'Teknik_Sartname_Örnek.pdf',
model: 'Gemini 3.6 Flash',
analyzedAt: new Date().toLocaleString('tr-TR'),
summary: 'İşbu teknik şartname, idarenin ihtiyacı olan medikal sarf malzemeleri ve cihaz parçalarının teknik özellikleri, garanti şartları, teslimat vadeleri ve muayene kabul kriterlerini düzenlemektedir.',
qualificationCriteria: [
'TSE HYB Belgesi ve Yetkili Satıcı Satış Yetki Belgesi',
'ISO 13485 Medikal Cihaz Kalite Yönetim Sertifikası',
'ÜTS (Ürün Takip Sistemi) Barkod ve TİUBB Kaydı',
'En az %50 Oranında Benzer İş Deneyim Belgesi (Kamu/Özel)',
'Son 3 yıla ait Bilanço ve Gelir Tablosu oranları'
],
criticalRequirements: [
'Ürünler biyo-uyumluluk (ISO 10993) standartlarına uygun olmalıdır.',
'Teslim edilen paketlerin sterilizasyon son kullanma tarihi en az 18 ay olmalıdır.',
'Arıza durumunda 24 saat içinde yerinde teknik servis desteği sağlanmalıdır.',
'Teslimat aşamasında her parti için numune test raporu (CoA) ibraz edilmelidir.'
],
risksAndPenalties: [
{ level: 'Yüksek', clause: 'Madde 14.2', text: 'Gecikilen her takvim günü için ihale bedelinin %0.5 oranında cezai şart uygulanacaktır.' },
{ level: 'Orta', clause: 'Madde 18.1', text: 'Numune kontrolünde standart dışı bulunan partiler 3 iş günü içinde muadili ile ücretsiz değiştirilmelidir.' },
{ level: 'Düşük', clause: 'Madde 22.4', text: 'Ambalaj üzerindeki Türkçe etiket eksikliğinde kabul işlemi durdurulacaktır.' }
],
bidAnalysis: {
estimatedCost: 185000,
recommendedBid: 148500,
minBidLimit: 132000,
maxBidLimit: 165000,
winProbability: 89,
competitionLevel: 'Orta Yoğunlukta (3-4 Firma Katılımı Bekleniyor)',
historicalDiscountAvg: '%19.7 EKAP İndirim Oranı'
}
};
// Win probability calculation based on custom bid slider
const calcProbability = (bid) => {
const rec = results.bidAnalysis.recommendedBid;
const diff = Math.abs(bid - rec);
const prob = Math.max(35, Math.min(96, Math.round(92 - (diff / 3000) * 3)));
return prob;
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/75 backdrop-blur-xs animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xl w-full max-w-4xl overflow-hidden my-auto flex flex-col max-h-[92vh]">
{/* Header */}
<div className="bg-linear-to-r from-blue-900 via-indigo-900 to-purple-900 text-white px-6 py-4 flex items-center justify-between border-b border-indigo-800">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-400 text-slate-900 flex items-center justify-center shadow-md font-black">
<Sparkles className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="font-extrabold text-base">Teknik Şartname & Teklif Analiz Asistanı</h2>
<span className="bg-amber-400 text-slate-900 text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-wider">
Gemini 3.6 Flash Engine
</span>
</div>
<p className="text-xs text-blue-200">Teknik şartname PDF'inizi yükleyin; yeterlik, riskler ve önerilen teklif fiyat aralığını saniyeler içinde çıkaralım.</p>
</div>
</div>
<button
onClick={() => setIsAiSpecAnalyzerOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Modal Content */}
<div className="p-6 space-y-6 overflow-y-auto">
{/* PDF Uploader Area */}
<div className="bg-slate-50 border-2 border-dashed border-blue-300 hover:border-blue-500 rounded-2xl p-6 text-center space-y-3 transition-colors">
<div className="w-12 h-12 rounded-2xl bg-blue-100 text-blue-600 flex items-center justify-center mx-auto shadow-xs">
<Upload className="w-6 h-6" />
</div>
<div>
<h3 className="font-bold text-sm text-slate-800">Teknik Şartname PDF Dosyası Yükleyin</h3>
<p className="text-xs text-slate-500">Bilgisayarınızdan `.pdf` dosyanızı buraya sürükleyin veya örnek ihale şartnamesi seçin.</p>
</div>
<div className="flex justify-center gap-3 pt-2">
<label className="bg-blue-600 hover:bg-blue-500 text-white font-extrabold text-xs px-5 py-2.5 rounded-xl cursor-pointer shadow-md transition-colors flex items-center gap-2">
<Upload className="w-4 h-4" />
<span>PDF Dosyası Seç</span>
<input
type="file"
accept=".pdf"
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
handleUploadAndAnalyze(e.target.files[0].name);
}
}}
/>
</label>
<button
type="button"
onClick={() => handleUploadAndAnalyze('Örnek_EKAP_Kayseri_Cezaevi_Teknik_Sartname.pdf')}
className="bg-white hover:bg-slate-100 text-slate-700 font-bold text-xs px-4 py-2.5 rounded-xl border border-slate-300 transition-colors flex items-center gap-2"
>
<FileText className="w-4 h-4 text-purple-600" />
<span>Örnek EKAP Şartnamesi İle Dene</span>
</button>
</div>
</div>
{/* Analyzing Loading Progress */}
{isAnalyzing && (
<div className="bg-linear-to-r from-blue-50 to-indigo-50 border border-blue-200 p-6 rounded-2xl space-y-3 animate-fade-in shadow-xs">
<div className="flex items-center justify-between font-extrabold text-xs text-blue-900">
<span className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
⚡ Gemini 3.6 Flash PDF Multimodal Akışı İşleniyor...
</span>
<span className="bg-amber-400 text-slate-900 px-2 py-0.5 rounded font-black text-[10px]">Gemini 3.6 Flash</span>
</div>
<div className="space-y-1.5 text-xs text-slate-700 font-medium pt-2">
<p className="flex items-center gap-2">✓ 📄 {selectedFile} belgesindeki maddeler ayrıştırıldı.</p>
<p className="flex items-center gap-2">✓ 🛡️ Yeterlik sertifikaları ve cezai hükümler tespit ediliyor...</p>
<p className="flex items-center gap-2">✓ 📊 EKAP geçmiş ihale veri tabanından optimal teklif marjı hesaplanıyor...</p>
</div>
</div>
)}
{/* AI Specification Report Output */}
{!isAnalyzing && results && (
<div className="space-y-6 animate-fade-in">
{/* Top Summary Banner */}
<div className="bg-slate-900 text-white p-5 rounded-2xl space-y-3 shadow-md">
<div className="flex items-center justify-between border-b border-slate-800 pb-2">
<span className="text-xs font-extrabold text-amber-400 uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-4 h-4" /> Gemini 3.6 Flash Şartname Özeti
</span>
<span className="text-[10px] text-slate-400">Analiz: {results.analyzedAt}</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed font-medium">{results.summary}</p>
</div>
{/* Bidding Recommendation Box (Kritik Fark Yaratacak Özellik) */}
<div className="bg-linear-to-br from-emerald-950 via-slate-900 to-blue-950 text-white p-6 rounded-2xl space-y-4 shadow-xl border border-emerald-800/40">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-emerald-400" />
<h3 className="font-black text-base">Gemini 3.6 Flash Önerilen İhale Teklif Aralığı</h3>
</div>
<span className="bg-emerald-500 text-slate-950 font-black text-xs px-3 py-1 rounded-full shadow-xs">
%{calcProbability(customBidSlider)} Kazanma Şansı
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">İdare Yaklaşık Maliyeti</span>
<span className="text-base font-black text-white mt-1 block">
₺{results.bidAnalysis.estimatedCost.toLocaleString('tr-TR')}
</span>
</div>
<div className="bg-emerald-500/20 p-3.5 rounded-xl border border-emerald-400/40">
<span className="text-[11px] text-emerald-300 font-bold block">Önerilen Optimal Teklif</span>
<span className="text-xl font-black text-amber-300 mt-0.5 block">
₺{results.bidAnalysis.recommendedBid.toLocaleString('tr-TR')}
</span>
</div>
<div className="bg-white/10 p-3.5 rounded-xl border border-white/10">
<span className="text-[11px] text-slate-300 font-medium block">Geçmiş EKAP İndirim Ort.</span>
<span className="text-base font-black text-blue-300 mt-1 block">
{results.bidAnalysis.historicalDiscountAvg}
</span>
</div>
</div>
{/* Custom Bid Price Interactive Slider */}
<div className="bg-black/30 p-4 rounded-xl space-y-2 border border-white/10">
<div className="flex justify-between items-center text-xs">
<span className="font-bold text-slate-300 flex items-center gap-1.5">
<Sliders className="w-4 h-4 text-amber-400" /> Teklif Fiyatınızı Simüle Edin:
</span>
<span className="font-black text-sm text-emerald-400">₺{customBidSlider.toLocaleString('tr-TR')}</span>
</div>
<input
type="range"
min={results.bidAnalysis.minBidLimit}
max={results.bidAnalysis.maxBidLimit}
step={500}
value={customBidSlider}
onChange={(e) => setCustomBidSlider(Number(e.target.value))}
className="w-full accent-emerald-500 cursor-pointer h-2 bg-slate-800 rounded-lg"
/>
<div className="flex justify-between text-[10px] text-slate-400 font-medium">
<span>Min Limit: ₺{results.bidAnalysis.minBidLimit.toLocaleString('tr-TR')}</span>
<span className="text-amber-300 font-bold">Önerilen: ₺{results.bidAnalysis.recommendedBid.toLocaleString('tr-TR')}</span>
<span>Maks Limit: ₺{results.bidAnalysis.maxBidLimit.toLocaleString('tr-TR')}</span>
</div>
</div>
</div>
{/* 2-Column Details: Qualifications & Risks */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Qualification Criteria */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
<CheckCircle2 className="w-4 h-4 text-emerald-600" /> Yeterlik Kriterleri & Zorunlu Belgeler
</h4>
<ul className="space-y-2 text-xs text-slate-700">
{results.qualificationCriteria.map((item, idx) => (
<li key={idx} className="flex items-start 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-emerald-600 mt-1.5 shrink-0"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
{/* Risks & Penalty Clauses */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-3">
<h4 className="font-extrabold text-sm text-slate-900 flex items-center gap-2 border-b border-slate-100 pb-2">
<AlertTriangle className="w-4 h-4 text-rose-600" /> Riskler & Şartname Cezai Hükümleri
</h4>
<div className="space-y-2 text-xs">
{results.risksAndPenalties.map((risk, idx) => (
<div key={idx} className="p-3 bg-slate-50 rounded-lg border border-slate-200 space-y-1">
<div className="flex items-center justify-between">
<span className="font-bold text-slate-900">{risk.clause}</span>
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded ${
risk.level === 'Yüksek' ? 'bg-red-100 text-red-800' :
risk.level === 'Orta' ? 'bg-amber-100 text-amber-800' : 'bg-slate-200 text-slate-800'
}`}>
{risk.level} Risk
</span>
</div>
<p className="text-slate-600 leading-snug">{risk.text}</p>
</div>
))}
</div>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="bg-slate-50 p-4 border-t border-slate-200 flex justify-end">
<button
onClick={() => setIsAiSpecAnalyzerOpen(false)}
className="bg-slate-800 hover:bg-slate-700 text-white font-bold text-xs px-6 py-2.5 rounded-xl transition-colors"
>
Kapat
</button>
</div>
</div>
</div>
);
};