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
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
*.log
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>İhaleist — Akıllı İhale Takip ve Analiz Platformu</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body class="bg-[#f3f5f8] text-slate-800 font-['Inter',sans-serif] antialiased selection:bg-blue-500 selection:text-white min-h-screen">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+2852
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "ihaleist-app",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"canvas-confetti": "^1.9.4",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^2.15.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"vite": "^6.0.7"
}
}
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { AppProvider, useApp } from './context/AppContext';
import { Header } from './components/Header';
import { LiveStatsBar } from './components/LiveStatsBar';
import { TopNavigation } from './components/TopNavigation';
import { SummaryCards } from './components/SummaryCards';
import { LeftSidebar } from './components/LeftSidebar';
import { TenderList } from './components/TenderList';
import { RightSidebar } from './components/RightSidebar';
import { SmartFilterBuilder } from './components/SmartFilterBuilder';
import { DetailedSearchModal } from './components/DetailedSearchModal';
import { TenderDetailModal } from './components/TenderDetailModal';
import { CompanyScannerModal } from './components/CompanyScannerModal';
import { AiSpecAnalyzerModal } from './components/AiSpecAnalyzerModal';
import { KanbanBoard } from './components/KanbanBoard';
import { DailyBulletin } from './components/tools/DailyBulletin';
import { ResultSearch } from './components/tools/ResultSearch';
import { TenderCalendar } from './components/tools/TenderCalendar';
import { ContractSearch } from './components/tools/ContractSearch';
import { VatCalculator } from './components/tools/VatCalculator';
import { PriceAnalysis } from './components/tools/PriceAnalysis';
import { CompetitorRadar } from './components/tools/CompetitorRadar';
import { DocumentVault } from './components/tools/DocumentVault';
import { AnalyticsDashboard } from './components/tools/AnalyticsDashboard';
import { ApiWebhooks } from './components/tools/ApiWebhooks';
import { PwaSettings } from './components/tools/PwaSettings';
const MainContent = () => {
const { activeTab } = useApp();
return (
<main className="max-w-[1600px] mx-auto px-4 pb-12">
{/* 3-Column Main View */}
{activeTab === 'list' && (
<>
<SummaryCards />
<div className="flex flex-col lg:flex-row gap-5 items-start">
<LeftSidebar />
<TenderList />
<RightSidebar />
</div>
</>
)}
{/* Dedicated Tool Views */}
{activeTab === 'smart_filter_builder' && <SmartFilterBuilder />}
{activeTab === 'bulten' && <DailyBulletin />}
{activeTab === 'sonuc' && <ResultSearch />}
{activeTab === 'takvim' && <TenderCalendar />}
{activeTab === 'sozlesme' && <ContractSearch />}
{activeTab === 'kdv' && <VatCalculator />}
{activeTab === 'fiyat' && <PriceAnalysis />}
{activeTab === 'kanban' && <KanbanBoard />}
{activeTab === 'rakip_radar' && <CompetitorRadar />}
{activeTab === 'dokuman_deposu' && <DocumentVault />}
{activeTab === 'analitik_dashboard' && <AnalyticsDashboard />}
{activeTab === 'api_webhooks' && <ApiWebhooks />}
{activeTab === 'pwa_settings' && <PwaSettings />}
{/* Global Modals */}
<DetailedSearchModal />
<TenderDetailModal />
<CompanyScannerModal />
<AiSpecAnalyzerModal />
</main>
);
};
export default function App() {
return (
<AppProvider>
<div className="min-h-screen bg-[#f3f5f8] text-slate-800 flex flex-col font-['Inter',sans-serif]">
<Header />
<LiveStatsBar />
<TopNavigation />
<MainContent />
</div>
</AppProvider>
);
}
+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>
);
};
+203
View File
@@ -0,0 +1,203 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { Globe, Bot, Sparkles, CheckCircle2, ShieldCheck, Tag, X, Loader2, Database, ArrowRight } from 'lucide-react';
import confetti from 'canvas-confetti';
export const CompanyScannerModal = () => {
const { isCompanyScannerOpen, setIsCompanyScannerOpen, userProfile, scanCompanyWebsite } = useApp();
const [urlInput, setUrlInput] = useState(userProfile.website || 'https://www.tanriverdimedikal.com.tr');
const [isScanning, setIsScanning] = useState(false);
const [scanStep, setScanStep] = useState(0);
const [scanSuccess, setScanSuccess] = useState(false);
if (!isCompanyScannerOpen) return null;
const handleStartScan = async (e) => {
e.preventDefault();
if (!urlInput.trim()) return;
setIsScanning(true);
setScanSuccess(false);
setScanStep(1);
setTimeout(() => setScanStep(2), 800);
setTimeout(() => setScanStep(3), 1600);
const capabilities = await scanCompanyWebsite(urlInput);
setIsScanning(false);
setScanSuccess(true);
confetti({ particleCount: 70, spread: 60 });
};
const caps = userProfile.scrapedCapabilities;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/70 backdrop-blur-xs animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xl w-full max-w-3xl overflow-hidden my-auto flex flex-col max-h-[92vh]">
{/* Header */}
<div className="bg-linear-to-r from-blue-900 via-slate-900 to-indigo-900 text-white px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white shadow-md">
<Globe className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-base flex items-center gap-2">
Firma Web Sitesi Tarama & AI Profilleme Motoru
</h2>
<p className="text-xs text-blue-200">Web sitenizi tarayarak ürettiğiniz ürün ve hizmetleri otomatik ihale veritabanına işleyin.</p>
</div>
</div>
<button
onClick={() => setIsCompanyScannerOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Modal Body */}
<div className="p-6 space-y-6 overflow-y-auto">
{/* URL Form */}
<form onSubmit={handleStartScan} className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-3">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">
FİRMANIZA AİT WEB SİTESİ ADRESİ
</label>
<div className="flex gap-2">
<div className="relative flex-1">
<Globe className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="url"
required
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="https://www.firmaniz.com.tr"
className="w-full bg-white border border-slate-300 rounded-lg pl-9 pr-4 py-2.5 text-xs text-slate-900 font-semibold focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
disabled={isScanning}
className="bg-blue-600 hover:bg-blue-500 text-white font-extrabold px-5 py-2.5 rounded-lg text-xs flex items-center gap-2 transition-colors shadow-md disabled:opacity-50 shrink-0"
>
{isScanning ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4 text-amber-300" />}
<span>{isScanning ? 'Tarama Sürüyor...' : 'Web Sitemi Tara & Profili İşle'}</span>
</button>
</div>
</form>
{/* Scanning Live Indicator */}
{isScanning && (
<div className="bg-blue-50 border border-blue-200 p-5 rounded-xl space-y-3 animate-fade-in">
<div className="flex items-center justify-between text-xs font-extrabold text-blue-900">
<span className="flex items-center gap-2">
<Loader2 className="w-4 h-4 text-blue-600 animate-spin" />
Yapay Zeka Taraması ve Otomatik Veri Çıkarımı Sürüyor...
</span>
<span>%{(scanStep * 33).toFixed(0)}</span>
</div>
<div className="w-full bg-blue-200 rounded-full h-2 overflow-hidden">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${scanStep * 33}%` }}
/>
</div>
<div className="space-y-1 text-xs text-slate-600 font-medium">
<p className={scanStep >= 1 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
1. 🌐 {urlInput} alan adına bağlanıldı ve HTML sayfaları alındı.
</p>
<p className={scanStep >= 2 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
2. 📄 "Hakkımızda", "Ürünler" ve "Belgelerimiz" sayfaları metin analizine tabi tutuldu.
</p>
<p className={scanStep >= 3 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
3. 💾 Faaliyet alanları ve sertifikalar çıkartılarak İhaleist veritabanına işlendi.
</p>
</div>
</div>
)}
{scanSuccess && (
<div className="bg-emerald-50 border border-emerald-200 text-emerald-900 p-4 rounded-xl text-xs font-bold flex items-center gap-2 animate-fade-in shadow-xs">
<CheckCircle2 className="w-5 h-5 text-emerald-600 shrink-0" />
<span>Web siteniz başarıyla tarandı ve firma yetkinlikleriniz veritabanına kaydedildi! İhale uyum skorunuz güncellendi.</span>
</div>
)}
{/* Current Extracted Capabilities Database View */}
{caps && (
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-slate-200 pb-2">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<Database className="w-4 h-4 text-blue-600" /> Veritabanındaki Kayıtlı Firma Profili
</h3>
<span className="text-[10px] text-slate-400 font-semibold">Son Tarama: {caps.scrapedAt}</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Sectors */}
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-2">
<span className="font-bold text-xs text-slate-800 uppercase tracking-wider flex items-center gap-1.5">
<Tag className="w-3.5 h-3.5 text-blue-600" /> Algılanan Sektörler
</span>
<div className="space-y-1">
{caps.extractedSectors?.map((s, i) => (
<span key={i} className="block text-xs font-semibold text-slate-700 bg-white border border-slate-200 p-1.5 rounded-lg">
{s}
</span>
))}
</div>
</div>
{/* Products */}
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-2">
<span className="font-bold text-xs text-slate-800 uppercase tracking-wider flex items-center gap-1.5">
<Bot className="w-3.5 h-3.5 text-emerald-600" /> Ürün & Hizmet Kataloğu
</span>
<div className="space-y-1">
{caps.extractedProducts?.map((p, i) => (
<span key={i} className="block text-xs font-semibold text-slate-700 bg-white border border-slate-200 p-1.5 rounded-lg">
📦 {p}
</span>
))}
</div>
</div>
{/* Certifications */}
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-2">
<span className="font-bold text-xs text-slate-800 uppercase tracking-wider flex items-center gap-1.5">
<ShieldCheck className="w-3.5 h-3.5 text-purple-600" /> Yetkinlik Belgeleri & ÜTS
</span>
<div className="space-y-1">
{caps.extractedCertifications?.map((c, i) => (
<span key={i} className="block text-xs font-semibold text-slate-700 bg-white border border-slate-200 p-1.5 rounded-lg">
📜 {c}
</span>
))}
</div>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="bg-slate-50 p-4 border-t border-slate-200 flex justify-end">
<button
onClick={() => setIsCompanyScannerOpen(false)}
className="bg-slate-800 hover:bg-slate-700 text-white font-bold text-xs px-6 py-2 rounded-lg transition-colors"
>
Tamam
</button>
</div>
</div>
</div>
);
};
+181
View File
@@ -0,0 +1,181 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { SECTORS_LIST, CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/mockTenders';
import { SlidersHorizontal, Search, RotateCcw, X } from 'lucide-react';
export const DetailedSearchModal = () => {
const {
isDetailedSearchOpen,
setIsDetailedSearchOpen,
searchQuery,
setSearchQuery,
selectedCity,
setSelectedCity,
selectedAlimTuru,
setSelectedAlimTuru,
selectedSektor,
setSelectedSektor,
selectedKurumTipi,
setSelectedKurumTipi,
onlyValidOnly,
setOnlyValidOnly,
setActiveTab
} = useApp();
const [iknQuery, setIknQuery] = useState('');
const [kurumQuery, setKurumQuery] = useState('');
if (!isDetailedSearchOpen) return null;
const handleApplySearch = (e) => {
e.preventDefault();
if (kurumQuery) setSearchQuery(kurumQuery);
else if (iknQuery) setSearchQuery(iknQuery);
setActiveTab('list');
setIsDetailedSearchOpen(false);
};
const handleReset = () => {
setSearchQuery('');
setSelectedCity('');
setSelectedAlimTuru('');
setSelectedSektor('');
setSelectedKurumTipi('');
setIknQuery('');
setKurumQuery('');
};
return (
<div className="fixed inset-[#00000080] z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-xs animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xl w-full max-w-2xl overflow-hidden">
{/* Header */}
<div className="bg-[#1f2937] text-white px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-2.5 font-bold text-base">
<SlidersHorizontal className="w-5 h-5 text-blue-400" />
<span>Detaylı İhale Arama</span>
</div>
<button
onClick={() => setIsDetailedSearchOpen(false)}
className="text-slate-400 hover:text-white p-1 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Form Body */}
<form onSubmit={handleApplySearch} className="p-6 space-y-4 text-xs">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block font-bold text-slate-700 mb-1">Anahtar Kelime</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Başlık, açıklama içinde aranır..."
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Alım Tipi</label>
<select
value={selectedAlimTuru}
onChange={(e) => setSelectedAlimTuru(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800"
>
<option value="">Tümü</option>
<option value="Doğrudan Temin">Doğrudan Temin</option>
<option value="Açık İhale">Açık İhale</option>
<option value="Pazarlık Usulü">Pazarlık Usulü</option>
</select>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Kurum Adı</label>
<input
type="text"
value={kurumQuery}
onChange={(e) => setKurumQuery(e.target.value)}
placeholder="Kurum adında aranır..."
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Kayıt No (İKN / DT No)</label>
<input
type="text"
value={iknQuery}
onChange={(e) => setIknQuery(e.target.value)}
placeholder="2026/... veya 26DT..."
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Sektör / Kategori</label>
<select
value={selectedSektor}
onChange={(e) => setSelectedSektor(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800"
>
<option value="">Tümü</option>
{SECTORS_LIST.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1">Şehir</label>
<select
value={selectedCity}
onChange={(e) => setSelectedCity(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-800"
>
<option value="">Tümü</option>
{CITIES_LIST.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>
<div className="pt-2">
<label className="flex items-center gap-2 cursor-pointer font-medium text-slate-700">
<input
type="checkbox"
checked={onlyValidOnly}
onChange={(e) => setOnlyValidOnly(e.target.checked)}
className="accent-blue-600 w-4 h-4 rounded"
/>
<span>Sadece teklif verilebilir ihaleleri göster (ihale tarihi geçmemiş ihaleler)</span>
</label>
</div>
{/* Footer Buttons */}
<div className="pt-4 border-t border-slate-200 flex items-center justify-center gap-4">
<button
type="submit"
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold px-8 py-2.5 rounded-xl shadow-md flex items-center gap-2 transition-colors"
>
<Search className="w-4 h-4" />
<span>İhaleleri Ara</span>
</button>
<button
type="button"
onClick={handleReset}
className="bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold px-5 py-2.5 rounded-xl border border-slate-300 flex items-center gap-2 transition-colors"
>
<RotateCcw className="w-4 h-4" />
<span>Temizle</span>
</button>
</div>
</form>
</div>
</div>
);
};
+187
View File
@@ -0,0 +1,187 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { Bot, Search, SlidersHorizontal, Gift, Bell, User, ChevronDown, CheckCircle, LogOut, Globe } from 'lucide-react';
export const Header = () => {
const {
searchQuery,
setSearchQuery,
setIsDetailedSearchOpen,
setIsCompanyScannerOpen,
setIsAiSpecAnalyzerOpen,
userProfile,
notifications,
setActiveTab,
clearAllFilters
} = useApp();
const [showNotifications, setShowNotifications] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
const handleSearchSubmit = (e) => {
e.preventDefault();
setActiveTab('list');
};
return (
<header className="bg-[#1f2937] text-white border-b border-slate-700 sticky top-0 z-40 shadow-sm">
<div className="max-w-[1600px] mx-auto px-4 h-16 flex items-center justify-between gap-4">
{/* Logo */}
<div
onClick={() => { setActiveTab('list'); clearAllFilters(); }}
className="flex items-center gap-2.5 cursor-pointer select-none group"
>
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center shadow-md group-hover:bg-blue-500 transition-colors">
<Bot className="w-6 h-6 text-white" />
</div>
<div className="flex flex-col">
<span className="font-black text-2xl tracking-tight text-white flex items-center gap-1">
İhale<span className="text-blue-400 font-extrabold">ist</span>
</span>
</div>
</div>
{/* Search Bar */}
<form onSubmit={handleSearchSubmit} className="flex-1 max-w-2xl flex items-center gap-2">
<div className="relative flex-1">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="İhale, kurum veya İKN no ile arayın..."
className="w-full bg-[#374151] text-white placeholder-slate-400 pl-4 pr-10 py-2 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 border border-slate-600 transition-all"
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white text-xs"
>
✕
</button>
)}
</div>
<button
type="submit"
className="bg-slate-700 hover:bg-slate-600 text-white font-medium px-4 py-2 rounded-lg text-sm flex items-center gap-1.5 transition-colors border border-slate-600 shrink-0"
>
<Search className="w-4 h-4" />
<span className="font-semibold text-xs tracking-wider">ARA</span>
</button>
<button
type="button"
onClick={() => setIsDetailedSearchOpen(true)}
title="Detaylı İhale Arama"
className="bg-blue-600 hover:bg-blue-500 text-white p-2 rounded-lg text-sm flex items-center justify-center transition-colors shadow-sm shrink-0"
>
<SlidersHorizontal className="w-4 h-4" />
</button>
</form>
{/* Right Action Icons */}
<div className="flex items-center gap-3">
{/* Gemini 3.6 Flash PDF Spec Analyzer Button */}
<button
onClick={() => setIsAiSpecAnalyzerOpen(true)}
className="bg-purple-600 hover:bg-purple-500 text-white font-extrabold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-xs transition-colors shrink-0"
title="Teknik Şartname PDF'ini Gemini 3.6 Flash İle Analiz Edin"
>
<Sparkles className="w-4 h-4 text-amber-300" />
<span className="hidden md:inline">Gemini 3.6 PDF Analizi</span>
</button>
{/* Company Web Scanner Button */}
<button
onClick={() => setIsCompanyScannerOpen(true)}
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-xs transition-colors shrink-0"
title="Web Sitenizi Tarayın & AI İhale Profiline İşleyin"
>
<Globe className="w-4 h-4 text-blue-200" />
<span className="hidden sm:inline">Web Siteni Tara (AI)</span>
</button>
{/* Trial Badge */}
<div className="bg-amber-400 text-slate-900 font-bold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-sm">
<Gift className="w-4 h-4 text-slate-900" />
<span>{userProfile.trialDaysLeft} gün</span>
</div>
{/* Notifications Dropdown */}
<div className="relative">
<button
onClick={() => setShowNotifications(!showNotifications)}
className="relative p-2 text-slate-300 hover:text-white hover:bg-slate-700 rounded-lg transition-colors"
>
<Bell className="w-5 h-5" />
{notifications.some(n => !n.read) && (
<span className="absolute top-1 right-1 w-2.5 h-2.5 bg-blue-500 rounded-full ring-2 ring-[#1f2937]" />
)}
</button>
{showNotifications && (
<div className="absolute right-0 mt-2 w-80 bg-white text-slate-800 rounded-xl shadow-xl border border-slate-200 py-2 z-50 animate-fade-in">
<div className="px-4 py-2 border-b border-slate-100 flex items-center justify-between">
<h4 className="font-bold text-sm">Bildirimler</h4>
<span className="text-xs text-blue-600 cursor-pointer hover:underline">Tümünü Okundu İşaretle</span>
</div>
<div className="max-h-64 overflow-y-auto divide-y divide-slate-100">
{notifications.map(n => (
<div key={n.id} className="p-3 hover:bg-slate-50 transition-colors cursor-pointer text-xs">
<div className="font-semibold text-slate-900 flex justify-between">
<span>{n.title}</span>
<span className="text-slate-400 font-normal text-[10px]">{n.time}</span>
</div>
<p className="text-slate-600 mt-1 leading-relaxed">{n.text}</p>
</div>
))}
</div>
</div>
)}
</div>
{/* User Profile */}
<div className="relative">
<button
onClick={() => setShowUserMenu(!showUserMenu)}
className="bg-slate-700 hover:bg-slate-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold flex items-center gap-2 transition-colors border border-slate-600"
>
<div className="w-6 h-6 rounded-full bg-blue-600 flex items-center justify-center text-white font-bold text-xs">
{userProfile.username.charAt(0).toUpperCase()}
</div>
<span>{userProfile.username}</span>
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
</button>
{showUserMenu && (
<div className="absolute right-0 mt-2 w-60 bg-white text-slate-800 rounded-xl shadow-xl border border-slate-200 py-2 z-50 animate-fade-in">
<div className="px-4 py-2 border-b border-slate-100">
<p className="font-bold text-sm text-slate-900">{userProfile.name}</p>
<p className="text-xs text-slate-500 truncate">{userProfile.company}</p>
</div>
<div className="py-1 text-xs">
<a href="#profile" className="px-4 py-2 hover:bg-slate-50 flex items-center gap-2 text-slate-700">
<User className="w-4 h-4 text-slate-400" /> Firma Bilgileri & Üyeler
</a>
<a href="#trial" className="px-4 py-2 hover:bg-slate-50 flex items-center gap-2 text-slate-700">
<Gift className="w-4 h-4 text-amber-500" /> Abonelik Planları (7 Gün Kalan)
</a>
<a href="#settings" className="px-4 py-2 hover:bg-slate-50 flex items-center gap-2 text-slate-700">
<CheckCircle className="w-4 h-4 text-slate-400" /> Bildirim Tercihleri
</a>
</div>
<div className="border-t border-slate-100 pt-1 text-xs">
<button className="w-full text-left px-4 py-2 text-red-600 hover:bg-red-50 flex items-center gap-2">
<LogOut className="w-4 h-4" /> Çıkış Yap
</button>
</div>
</div>
)}
</div>
</div>
</div>
</header>
);
};
+174
View File
@@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { Kanban, User, Calendar, MapPin, Star, Plus, ArrowRight, Eye } from 'lucide-react';
export const KanbanBoard = () => {
const { tenders, updateKanbanStatus, setActiveTenderDetail } = useApp();
const [selectedPersonel, setSelectedPersonel] = useState('');
const [draggedTenderId, setDraggedTenderId] = useState(null);
const columns = [
{ id: 'incelenecek', title: 'İncelenecek', color: 'bg-slate-100 border-slate-300 text-slate-800' },
{ id: 'inceleniyor', title: 'İnceleniyor', color: 'bg-blue-50 border-blue-200 text-blue-900' },
{ id: 'teklif_hazirlaniyor', title: 'Teklif Hazırlanıyor', color: 'bg-amber-50 border-amber-200 text-amber-900' },
{ id: 'teklif_verildi', title: 'Teklif Verildi', color: 'bg-purple-50 border-purple-200 text-purple-900' },
{ id: 'kazanildi', title: 'Kazanıldı', color: 'bg-emerald-50 border-emerald-200 text-emerald-900' },
{ id: 'kaybedildi', title: 'Kaybedildi', color: 'bg-rose-50 border-rose-200 text-rose-900' }
];
const personnelList = ['Hikmet Tanrıverdi', 'Ahmet Yılmaz', 'Zeynep Kaya', 'Mehmet Demir'];
const filteredTenders = tenders.filter(t => {
if (!t.takipte && !t.kanbanStatus) return false;
if (selectedPersonel && t.atananPersonel !== selectedPersonel) return false;
return true;
});
const handleDragStart = (e, id) => {
e.dataTransfer.setData('tenderId', id);
setDraggedTenderId(id);
};
const handleDragOver = (e) => {
e.preventDefault();
};
const handleDrop = (e, columnId) => {
e.preventDefault();
const id = e.dataTransfer.getData('tenderId') || draggedTenderId;
if (id) {
updateKanbanStatus(id, columnId);
setDraggedTenderId(null);
}
};
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Kanban Header Controls */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-600 text-white flex items-center justify-center shadow-md">
<Kanban className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Ekip Kanban Takip Panosu</h1>
<p className="text-xs text-slate-500">Kartları kolonlar arasında sürükleyerek ihale süreçlerinizi yönetin.</p>
</div>
</div>
{/* Personnel Filter & Slack Notification Buttons */}
<div className="flex items-center gap-3 w-full sm:w-auto">
<div className="flex items-center gap-2 bg-slate-50 border border-slate-300 rounded-xl px-3 py-1.5 text-xs">
<User className="w-4 h-4 text-slate-400" />
<select
value={selectedPersonel}
onChange={(e) => setSelectedPersonel(e.target.value)}
className="bg-transparent font-semibold text-slate-800 outline-none"
>
<option value="">Tüm Personeller</option>
{personnelList.map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<button
onClick={() => alert('Slack & Teams kanalına Kanban güncel durumu başarıyla gönderildi!')}
className="bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center gap-1.5 transition-colors shadow-xs shrink-0"
title="Slack & Teams Ekip Kanalına Bildirim Gönder"
>
<span>💬 Slack/Teams'e Gönder</span>
</button>
</div>
</div>
{/* Kanban Columns Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3.5 overflow-x-auto pb-4">
{columns.map(col => {
const colTenders = filteredTenders.filter(t => (t.kanbanStatus || 'incelenecek') === col.id);
return (
<div
key={col.id}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, col.id)}
className={`rounded-2xl border ${col.color} p-3 flex flex-col min-h-[550px] shadow-2xs transition-colors`}
>
{/* Column Header */}
<div className="flex items-center justify-between border-b border-black/10 pb-2 mb-3">
<span className="font-extrabold text-xs uppercase tracking-tight">{col.title}</span>
<span className="bg-white/80 font-black text-xs px-2 py-0.5 rounded-full border border-black/10 shadow-2xs">
{colTenders.length}
</span>
</div>
{/* Column Cards */}
<div className="space-y-3 flex-1 overflow-y-auto max-h-[650px] pr-0.5">
{colTenders.length === 0 ? (
<div className="py-10 text-center border-2 border-dashed border-black/10 rounded-xl">
<p className="text-[11px] font-semibold text-slate-400">İhale yok</p>
</div>
) : (
colTenders.map(t => (
<div
key={t.id}
draggable
onDragStart={(e) => handleDragStart(e, t.id)}
className="bg-white rounded-xl p-3 border border-slate-200 shadow-xs hover:shadow-md cursor-grab active:cursor-grabbing space-y-2 transition-all group"
>
<div className="flex items-start justify-between gap-1">
<span className="text-[10px] font-extrabold bg-blue-100 text-blue-800 px-2 py-0.5 rounded">
#{t.ikn}
</span>
<button
onClick={() => setActiveTenderDetail(t)}
className="text-slate-400 hover:text-blue-600 p-0.5"
title="Detayı Aç"
>
<Eye className="w-3.5 h-3.5" />
</button>
</div>
<h4
onClick={() => setActiveTenderDetail(t)}
className="font-bold text-xs text-slate-900 line-clamp-2 hover:text-blue-600 cursor-pointer leading-snug"
>
{t.baslik}
</h4>
<div className="flex items-center justify-between text-[10px] text-slate-500 font-medium">
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3 text-rose-500" /> {t.sehir}
</span>
<span className="flex items-center gap-1 font-bold text-blue-900">
<Calendar className="w-3 h-3 text-blue-500" /> {t.tarih}
</span>
</div>
{/* Personnel Select on Card */}
<div className="pt-1.5 border-t border-slate-100 flex items-center justify-between text-[10px]">
<span className="text-slate-400">Atanan:</span>
<select
value={t.atananPersonel || ''}
onChange={(e) => updateKanbanStatus(t.id, col.id, e.target.value)}
className="bg-slate-50 border border-slate-200 rounded px-1.5 py-0.5 text-[10px] font-bold text-slate-800"
>
<option value="">Atama Yap</option>
{personnelList.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
))
)}
</div>
</div>
);
})}
</div>
</div>
);
};
+288
View File
@@ -0,0 +1,288 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { SECTORS_LIST, INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/mockTenders';
import {
Compass, Zap, Wrench, Calendar, Eye, EyeOff, FileText, CheckCircle2, XCircle, Archive,
Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag
} from 'lucide-react';
export const LeftSidebar = () => {
const {
quickFilter,
setQuickFilter,
setActiveTab,
savedFilters,
deleteSavedFilter,
selectedAlimTuru,
setSelectedAlimTuru,
selectedSektor,
setSelectedSektor,
selectedKurumTipi,
setSelectedKurumTipi,
selectedCity,
setSelectedCity,
clearAllFilters
} = useApp();
const [openSections, setOpenSections] = useState({
alimTuru: true,
sektorler: true,
kurumTipleri: false,
iller: false
});
const toggleSection = (key) => {
setOpenSections(prev => ({ ...prev, [key]: !prev[key] }));
};
const quickNavItems = [
{ id: 'all', label: 'Güncel Tüm İhaleler', icon: Compass },
{ id: 'dogrudan_temin', label: 'Doğrudan Temin', icon: Zap, badge: 'Popüler' },
{ id: 'ihaleler', label: 'İhaleler (Açık/Pazarlık)', icon: Wrench },
{ id: 'bugun_eklenenler', label: 'Bugün Eklenenler', icon: Calendar },
{ id: 'incelenenler', label: 'İncelediğim İhaleler', icon: Eye },
{ id: 'incelenmedi', label: 'İncelemediklerim', icon: EyeOff },
{ id: 'sozlesmeler', label: 'Sözleşme İmzalananlar', icon: FileText },
{ id: 'sonuclananlar', label: 'Sonuçlananlar', icon: CheckCircle2 },
{ id: 'iptal', label: 'İptal Edilenler', icon: XCircle },
{ id: 'arsiv', label: 'İhale Arşivi', icon: Archive }
];
return (
<aside className="w-full lg:w-72 shrink-0 space-y-4">
{/* Hızlı Erişim Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100 bg-slate-50 flex items-center justify-between">
<span className="font-bold text-xs uppercase tracking-wider text-slate-700 flex items-center gap-1.5">
<Compass className="w-4 h-4 text-blue-600" /> Hızlı Erişim
</span>
{(quickFilter !== 'all' || selectedSektor || selectedCity || selectedAlimTuru || selectedKurumTipi) && (
<button
onClick={clearAllFilters}
className="text-[11px] font-medium text-blue-600 hover:underline"
>
Filtreleri Sıfırla
</button>
)}
</div>
<div className="p-1.5 divide-y divide-slate-100/50">
{quickNavItems.map(item => {
const Icon = item.icon;
const isSelected = quickFilter === item.id;
return (
<button
key={item.id}
onClick={() => {
setQuickFilter(item.id);
setActiveTab('list');
}}
className={`w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-semibold transition-colors text-left ${
isSelected
? 'bg-blue-600 text-white shadow-xs'
: 'text-slate-700 hover:bg-slate-100'
}`}
>
<div className="flex items-center gap-2.5">
<Icon className={`w-4 h-4 ${isSelected ? 'text-white' : 'text-slate-400'}`} />
<span>{item.label}</span>
</div>
{item.badge && (
<span className={`text-[10px] px-1.5 py-0.5 rounded-full font-bold ${
isSelected ? 'bg-white/20 text-white' : 'bg-amber-100 text-amber-800'
}`}>
{item.badge}
</span>
)}
</button>
);
})}
</div>
</div>
{/* Kayıtlı Filtrelerim (Akıllı Filtre Botu) Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-4">
<div className="flex items-center justify-between mb-3">
<span className="font-bold text-xs uppercase tracking-wider text-slate-800 flex items-center gap-1.5">
<Bot className="w-4 h-4 text-blue-600" /> Kayıtlı Filtrelerim
</span>
<span className="text-[10px] bg-blue-100 text-blue-800 font-bold px-2 py-0.5 rounded-full">
{savedFilters.length} Bot
</span>
</div>
{savedFilters.length === 0 ? (
<div className="py-4 text-center">
<Bot className="w-8 h-8 text-slate-300 mx-auto mb-1.5" />
<p className="text-xs text-slate-400">Henüz aktif bir filtre yok.</p>
</div>
) : (
<div className="space-y-2 mb-3">
{savedFilters.map(filter => (
<div
key={filter.id}
className="bg-slate-50 hover:bg-slate-100 p-2.5 rounded-lg border border-slate-200 flex items-center justify-between text-xs transition-colors group"
>
<div className="flex-1 min-w-0 pr-2">
<p className="font-bold text-slate-800 truncate">{filter.name}</p>
<p className="text-[10px] text-slate-500 truncate mt-0.5">
👤 {filter.user} {filter.sehir && `• 📍 ${filter.sehir}`}
</p>
</div>
<button
onClick={() => deleteSavedFilter(filter.id)}
title="Filtreyi Sil"
className="text-slate-400 hover:text-red-600 p-1 opacity-60 group-hover:opacity-100 transition-opacity"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
<button
onClick={() => setActiveTab('smart_filter_builder')}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-3 rounded-lg text-xs flex items-center justify-center gap-2 transition-colors shadow-xs"
>
<PlusCircle className="w-4 h-4" />
<span>Yeni Filtre Kur</span>
</button>
</div>
{/* Accordion Filters */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs overflow-hidden divide-y divide-slate-100">
{/* Alım Türü */}
<div>
<button
onClick={() => toggleSection('alimTuru')}
className="w-full px-4 py-3 flex items-center justify-between text-xs font-bold text-slate-800 bg-slate-50/50 hover:bg-slate-100 transition-colors"
>
<span className="flex items-center gap-2">
<Filter className="w-4 h-4 text-blue-600" /> Alım Türü
</span>
{openSections.alimTuru ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
</button>
{openSections.alimTuru && (
<div className="p-3 space-y-1.5 text-xs">
{['Tümü', 'Doğrudan Temin', 'Açık İhale', 'Pazarlık Usulü'].map(turu => (
<label key={turu} className="flex items-center gap-2 cursor-pointer hover:bg-slate-50 p-1.5 rounded-md">
<input
type="radio"
name="alimTuru"
checked={(turu === 'Tümü' && !selectedAlimTuru) || selectedAlimTuru === turu}
onChange={() => {
setSelectedAlimTuru(turu === 'Tümü' ? '' : turu);
setActiveTab('list');
}}
className="accent-blue-600"
/>
<span className="text-slate-700 font-medium">{turu}</span>
</label>
))}
</div>
)}
</div>
{/* Sektörler */}
<div>
<button
onClick={() => toggleSection('sektorler')}
className="w-full px-4 py-3 flex items-center justify-between text-xs font-bold text-slate-800 bg-slate-50/50 hover:bg-slate-100 transition-colors"
>
<span className="flex items-center gap-2">
<Tag className="w-4 h-4 text-blue-600" /> Sektörler
</span>
{openSections.sektorler ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
</button>
{openSections.sektorler && (
<div className="p-3 space-y-1 text-xs max-h-48 overflow-y-auto">
<button
onClick={() => { setSelectedSektor(''); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded font-medium ${!selectedSektor ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
Tüm Sektörler
</button>
{SECTORS_LIST.map(sec => (
<button
key={sec}
onClick={() => { setSelectedSektor(sec); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded truncate font-medium ${selectedSektor === sec ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
{sec}
</button>
))}
</div>
)}
</div>
{/* Kurum Tipleri */}
<div>
<button
onClick={() => toggleSection('kurumTipleri')}
className="w-full px-4 py-3 flex items-center justify-between text-xs font-bold text-slate-800 bg-slate-50/50 hover:bg-slate-100 transition-colors"
>
<span className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-600" /> Kurum Tipleri
</span>
{openSections.kurumTipleri ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
</button>
{openSections.kurumTipleri && (
<div className="p-3 space-y-1 text-xs max-h-48 overflow-y-auto">
<button
onClick={() => { setSelectedKurumTipi(''); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded font-medium ${!selectedKurumTipi ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
Tüm Kurumlar
</button>
{INSTITUTION_TYPES_LIST.map(k => (
<button
key={k}
onClick={() => { setSelectedKurumTipi(k); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded truncate font-medium ${selectedKurumTipi === k ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
{k}
</button>
))}
</div>
)}
</div>
{/* İl Dağılımları */}
<div>
<button
onClick={() => toggleSection('iller')}
className="w-full px-4 py-3 flex items-center justify-between text-xs font-bold text-slate-800 bg-slate-50/50 hover:bg-slate-100 transition-colors"
>
<span className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-blue-600" /> Şehir Seçimi
</span>
{openSections.iller ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
</button>
{openSections.iller && (
<div className="p-3 space-y-1 text-xs max-h-48 overflow-y-auto">
<button
onClick={() => { setSelectedCity(''); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded font-medium ${!selectedCity ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
Tüm Şehirler
</button>
{CITIES_LIST.map(city => (
<button
key={city}
onClick={() => { setSelectedCity(city); setActiveTab('list'); }}
className={`w-full text-left px-2 py-1 rounded font-medium ${selectedCity === city ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
>
{city}
</button>
))}
</div>
)}
</div>
</div>
</aside>
);
};
+43
View File
@@ -0,0 +1,43 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { Database, PlusCircle, Eye } from 'lucide-react';
export const LiveStatsBar = () => {
const { liveStats } = useApp();
return (
<div className="bg-[#111827] text-slate-300 py-1.5 px-4 text-xs font-medium border-b border-slate-800 shadow-inner">
<div className="max-w-[1600px] mx-auto flex items-center justify-center gap-6 sm:gap-10 flex-wrap">
{/* Online Kullanıcı */}
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span className="font-bold text-white tracking-wide">{liveStats.onlineKullanici.toLocaleString('tr-TR')}</span>
<span className="text-slate-400">Online Kullanıcı</span>
</div>
{/* Toplam Kalem */}
<div className="flex items-center gap-1.5">
<Database className="w-3.5 h-3.5 text-blue-400" />
<span className="font-bold text-white tracking-wide">{liveStats.toplamKalem.toLocaleString('tr-TR')}</span>
<span className="text-slate-400">Toplam Kalem</span>
</div>
{/* Bugün Eklenen */}
<div className="flex items-center gap-1.5">
<PlusCircle className="w-3.5 h-3.5 text-emerald-400" />
<span className="font-bold text-emerald-400 tracking-wide">{liveStats.bugunEklenen.toLocaleString('tr-TR')}</span>
<span className="text-slate-400">Bugün Eklenen</span>
</div>
{/* Aktif Takip */}
<div className="flex items-center gap-1.5">
<Eye className="w-3.5 h-3.5 text-amber-400" />
<span className="font-bold text-white tracking-wide">{liveStats.aktifTakip.toLocaleString('tr-TR')}</span>
<span className="text-slate-400">Aktif Takip</span>
</div>
</div>
</div>
);
};
+114
View File
@@ -0,0 +1,114 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { MOCK_COMPANY_STATS, COMPETITOR_DATA } from '../data/mockTenders';
import { Briefcase, FileText, Calendar, History, Trophy, Eye, Kanban, Radar, ArrowRight } from 'lucide-react';
export const RightSidebar = () => {
const { setActiveTab, setQuickFilter, tenders } = useApp();
const trackedCount = tenders.filter(t => t.takipte).length;
const records = [
{ label: 'Notlarım', count: MOCK_COMPANY_STATS.notlarimCount, icon: FileText, filterKey: 'all' },
{ label: 'Ajanda', count: MOCK_COMPANY_STATS.ajandaCount, icon: Calendar, filterKey: 'all' },
{ label: 'İncelediklerim', count: MOCK_COMPANY_STATS.incelediklerimCount, icon: History, filterKey: 'incelenenler' },
{ label: 'Sözleşmelerim', count: MOCK_COMPANY_STATS.sozlesmelerimCount, icon: FileText, filterKey: 'sozlesmeler' },
{ label: 'Sonuçlarım', count: MOCK_COMPANY_STATS.sonuclarimCount, icon: Trophy, filterKey: 'sonuclananlar' },
{ label: 'Takibimdekiler', count: trackedCount, icon: Eye, filterKey: 'takiptekiler' }
];
return (
<aside className="w-full lg:w-80 shrink-0 space-y-4">
{/* Firmanıza Ait Kayıtlar */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100 bg-slate-50 flex items-center justify-between">
<span className="font-bold text-xs uppercase tracking-wider text-slate-800 flex items-center gap-2">
<Briefcase className="w-4 h-4 text-blue-600" /> Firmanıza Ait Kayıtlar
</span>
</div>
<div className="p-2 space-y-1">
{records.map(rec => {
const Icon = rec.icon;
return (
<div
key={rec.label}
onClick={() => {
setQuickFilter(rec.filterKey);
setActiveTab('list');
}}
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-slate-50 cursor-pointer transition-colors text-xs font-semibold text-slate-700"
>
<div className="flex items-center gap-2.5">
<Icon className="w-4 h-4 text-slate-400" />
<span>{rec.label}</span>
</div>
<span className="bg-slate-100 text-slate-800 font-bold px-2 py-0.5 rounded-md border border-slate-200">
{rec.count}
</span>
</div>
);
})}
</div>
</div>
{/* İhale Durumlarım (Kanban Entrance) */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-4 space-y-3">
<div className="flex items-center gap-2 font-bold text-xs uppercase tracking-wider text-slate-800 border-b border-slate-100 pb-2">
<Kanban className="w-4 h-4 text-blue-600" /> İhale Durumlarım
</div>
<p className="text-xs text-slate-500 leading-relaxed">
Takibe aldığınız ihalelere durum atayabilir, ekip içinde sürükle-bırak ile iş dağıtabilirsiniz.
</p>
<button
onClick={() => setActiveTab('kanban')}
className="w-full bg-[#1f2937] hover:bg-slate-800 text-white font-bold py-2.5 px-4 rounded-lg text-xs flex items-center justify-center gap-2 transition-colors shadow-xs"
>
<Kanban className="w-4 h-4 text-amber-400" />
<span>Kanban Panosunu Aç</span>
</button>
</div>
{/* Rakip Radar Widget */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-4 space-y-3">
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
<span className="font-bold text-xs uppercase tracking-wider text-slate-800 flex items-center gap-2">
<Radar className="w-4 h-4 text-indigo-600" /> Rakip Radar & Pazar Payı
</span>
<span className="bg-indigo-100 text-indigo-800 text-[10px] font-bold px-2 py-0.5 rounded-full">
Sektör Analizi
</span>
</div>
<div className="space-y-2.5 text-xs">
{COMPETITOR_DATA.slice(0, 3).map((comp, i) => (
<div key={i} className="space-y-1">
<div className="flex items-center justify-between text-[11px] font-semibold text-slate-700">
<span className="truncate max-w-[170px]">{comp.name}</span>
<span className="font-bold text-indigo-900">%{comp.share} ({comp.winCount} İhale)</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden">
<div
className="bg-indigo-600 h-1.5 rounded-full"
style={{ width: `${comp.share}%` }}
/>
</div>
</div>
))}
</div>
<button
onClick={() => setActiveTab('sozlesme')}
className="w-full text-indigo-600 hover:text-indigo-800 font-bold text-xs py-1.5 flex items-center justify-center gap-1 hover:underline"
>
<span>Tüm Rakip Sözleşmelerini İncele</span>
<ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
</aside>
);
};
+275
View File
@@ -0,0 +1,275 @@
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { INSTITUTION_TYPES_LIST, CITIES_LIST, SECTORS_LIST } from '../data/mockTenders';
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
export const SmartFilterBuilder = () => {
const { savedFilters, addSavedFilter, deleteSavedFilter, setActiveTab } = useApp();
const [filterName, setFilterName] = useState('');
const [selectedKurumTip, setSelectedKurumTip] = useState('');
const [selectedUsul, setSelectedUsul] = useState('');
const [selectedCity, setSelectedCity] = useState('');
const [selectedRegion, setSelectedRegion] = useState('');
const [selectedSektor, setSelectedSektor] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const regions = [
'AKDENİZ BÖLGESİ', 'DOĞU ANADOLU BÖLGESİ', 'EGE BÖLGESİ',
'GÜNEYDOĞU ANADOLU BÖLGESİ', 'İÇ ANADOLU BÖLGESİ', 'KARADENİZ BÖLGESİ', 'MARMARA BÖLGESİ'
];
const usuller = [
'Açık İhale', 'Dış Alım', 'Doğrudan Temin', 'İstisna', 'Pazarlık Usulü', 'Yaklaşık Maliyet'
];
const handleCreateFilter = (e) => {
e.preventDefault();
if (!filterName.trim()) {
alert('Lütfen bir filtre adı giriniz.');
return;
}
addSavedFilter({
name: filterName.trim(),
kurumTipi: selectedKurumTip,
usul: selectedUsul,
sehir: selectedCity,
bolge: selectedRegion,
sektor: selectedSektor
});
setSuccessMessage('Yeni akıllı filtre başarıyla oluşturuldu.');
setFilterName('');
setTimeout(() => setSuccessMessage(''), 4000);
};
return (
<div className="max-w-[1600px] mx-auto my-4 space-y-4 animate-fade-in">
{/* Top Banner */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shadow-md">
<Bot className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Akıllı Filtre (Bot) Kurulumu</h1>
<p className="text-xs text-slate-500">Sektör veya şehrinize uygun yeni ihale eklendiğinde anında haberiniz olsun.</p>
</div>
</div>
<button
onClick={() => setActiveTab('list')}
className="bg-slate-800 hover:bg-slate-700 text-white font-semibold text-xs px-4 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-xs"
>
<HelpCircle className="w-4 h-4" />
<span>Nasıl Çalışır?</span>
</button>
</div>
{successMessage && (
<div className="bg-emerald-50 border border-emerald-200 text-emerald-800 px-4 py-3 rounded-xl text-xs font-semibold flex items-center gap-2 animate-fade-in shadow-xs">
<CheckCircle2 className="w-5 h-5 text-emerald-600 shrink-0" />
<span>{successMessage}</span>
</div>
)}
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Left Column: Ekip Filtre Listesi */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-4 h-fit">
<div className="flex items-center justify-between mb-4 pb-2 border-b border-slate-100">
<h3 className="font-bold text-xs uppercase tracking-wider text-slate-800 flex items-center gap-2">
<Bot className="w-4 h-4 text-blue-600" /> Ekip Filtre Listesi
</h3>
<span className="bg-blue-100 text-blue-800 text-[10px] font-bold px-2 py-0.5 rounded-full">
{savedFilters.length}
</span>
</div>
<div className="space-y-2 mb-4">
{savedFilters.map(f => (
<div key={f.id} className="p-3 bg-slate-50 border border-slate-200 rounded-lg text-xs hover:bg-slate-100 transition-colors">
<div className="flex items-center justify-between font-bold text-slate-900">
<span>🤖 {f.name}</span>
<button
onClick={() => deleteSavedFilter(f.id)}
className="text-slate-400 hover:text-red-600 text-xs"
>
🗑️
</button>
</div>
<p className="text-[10px] text-slate-500 mt-1">👤 {f.user} {f.sehir && `• 📍 ${f.sehir}`}</p>
</div>
))}
</div>
<button
onClick={() => setActiveTab('list')}
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 transition-colors border border-slate-200"
>
<ArrowLeft className="w-4 h-4" />
<span>Ana Ekrana Dön</span>
</button>
</div>
{/* Right 3 Columns: Filter Form */}
<div className="lg:col-span-3 space-y-6">
<form onSubmit={handleCreateFilter} className="space-y-6">
{/* Filter Name Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-5 space-y-2">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">
FİLTRE ADI (ZORUNLU)
</label>
<input
type="text"
required
value={filterName}
onChange={(e) => setFilterName(e.target.value)}
placeholder="Örn: Ege Bölgesi Cihaz İhaleleri"
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-4 py-2.5 text-sm text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-[11px] text-slate-400">Bu isim sadece sizin takip ve bildirim akışınızda gösterilecektir.</p>
</div>
{/* Middle Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Kurum Tipi & İhale Usulü */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-5 space-y-4">
<div className="flex items-center gap-2 border-b border-slate-100 pb-3">
<Building2 className="w-5 h-5 text-blue-600" />
<h3 className="font-bold text-sm text-slate-800">Kurum Tipi & İhale Usulü</h3>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">KURUM TİPLERİ</label>
<div className="flex flex-wrap gap-1.5 mb-2">
{['Sağlık', 'Belediye', 'Üniversite', 'Kamu', 'Temizle'].map(badge => (
<button
key={badge}
type="button"
onClick={() => {
if (badge === 'Temizle') setSelectedKurumTip('');
else if (badge === 'Sağlık') setSelectedKurumTip('Eğitim ve Araştırma Hastaneleri');
else if (badge === 'Belediye') setSelectedKurumTip('Belediyeler');
else if (badge === 'Üniversite') setSelectedKurumTip('Üniversiteler');
else setSelectedKurumTip('Bakanlıklar');
}}
className="bg-red-50 text-red-600 hover:bg-red-100 text-[11px] font-bold px-2.5 py-1 rounded-md border border-red-200 transition-colors"
>
{badge}
</button>
))}
</div>
<select
value={selectedKurumTip}
onChange={(e) => setSelectedKurumTip(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
>
<option value="">Tüm Kurum Tipleri</option>
{INSTITUTION_TYPES_LIST.map(k => (
<option key={k} value={k}>{k}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">İHALE USULLERİ</label>
<div className="space-y-1.5 max-h-40 overflow-y-auto pr-1">
{usuller.map(usul => (
<label key={usul} className="flex items-center gap-2 text-xs text-slate-700 cursor-pointer hover:bg-slate-50 p-1 rounded">
<input
type="radio"
name="usul_radio"
checked={selectedUsul === usul}
onChange={() => setSelectedUsul(usul)}
className="accent-blue-600"
/>
<span>{usul}</span>
</label>
))}
</div>
</div>
</div>
{/* Konum Seçimi */}
<div className="bg-white rounded-xl border border-slate-200 shadow-xs p-5 space-y-4">
<div className="flex items-center gap-2 border-b border-slate-100 pb-3">
<MapPin className="w-5 h-5 text-blue-600" />
<h3 className="font-bold text-sm text-slate-800">Konum Seçimi</h3>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">BÖLGELER</label>
<select
value={selectedRegion}
onChange={(e) => setSelectedRegion(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
>
<option value="">Tüm Bölgeler</option>
{regions.map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">ŞEHİRLER</label>
<select
value={selectedCity}
onChange={(e) => setSelectedCity(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
>
<option value="">Tüm Şehirler</option>
{CITIES_LIST.map(city => (
<option key={city} value={city}>{city}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">SEKTÖR</label>
<select
value={selectedSektor}
onChange={(e) => setSelectedSektor(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-xs text-slate-800"
>
<option value="">Tüm Sektörler</option>
{SECTORS_LIST.map(s => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={() => setActiveTab('list')}
className="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold px-5 py-2.5 rounded-lg text-xs transition-colors"
>
İptal
</button>
<button
type="submit"
className="bg-blue-600 hover:bg-blue-500 text-white font-bold px-6 py-2.5 rounded-lg text-xs transition-colors shadow-md flex items-center gap-2"
>
<Bot className="w-4 h-4" />
<span>Akıllı Filtre Botunu Kaydet & Çalıştır</span>
</button>
</div>
</form>
</div>
</div>
</div>
);
};
+89
View File
@@ -0,0 +1,89 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { Layers, CalendarPlus, Star, BellRing, EyeOff } from 'lucide-react';
export const SummaryCards = () => {
const { tenders, quickFilter, setQuickFilter, setActiveTab } = useApp();
const activeCount = tenders.filter(t => t.durum === 'Aktif').length;
const todayCount = tenders.filter(t => t.eklenmeZamani.includes('önce') || t.eklenmeZamani.includes('az')).length;
const trackedCount = tenders.filter(t => t.takipte).length;
const sinceLastLoginCount = 14; // Mocked counter
const unreadCount = tenders.filter(t => !t.incelendi).length;
const cards = [
{
id: 'all',
label: 'AKTİF İHALELER',
count: activeCount,
icon: Layers,
color: 'blue'
},
{
id: 'bugun_eklenenler',
label: 'BUGÜN EKLENENLER',
count: todayCount,
icon: CalendarPlus,
color: 'emerald'
},
{
id: 'takiptekiler',
label: 'TAKİP ETTİKLERİM',
count: trackedCount,
icon: Star,
color: 'amber'
},
{
id: 'son_giris',
label: 'SON GİRİŞTEN BERİ',
count: sinceLastLoginCount,
icon: BellRing,
color: 'purple'
},
{
id: 'incelenmedi',
label: 'İNCELENMEMİŞ',
count: unreadCount,
icon: EyeOff,
color: 'rose'
}
];
return (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3.5 my-4">
{cards.map(card => {
const Icon = card.icon;
const isSelected = quickFilter === card.id;
return (
<div
key={card.id}
onClick={() => {
setQuickFilter(card.id);
setActiveTab('list');
}}
className={`bg-white rounded-xl p-4 border transition-all cursor-pointer select-none shadow-xs flex items-center justify-between group hover:shadow-md ${
isSelected
? 'border-blue-600 ring-2 ring-blue-500/20 bg-blue-50/20'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div>
<p className="text-[11px] font-bold tracking-wider text-slate-500 uppercase">{card.label}</p>
<p className="text-2xl font-black text-slate-900 mt-1">{card.count}</p>
</div>
<div className={`w-11 h-11 rounded-xl flex items-center justify-center transition-transform group-hover:scale-105 ${
card.color === 'blue' ? 'bg-blue-100 text-blue-600' :
card.color === 'emerald' ? 'bg-emerald-100 text-emerald-600' :
card.color === 'amber' ? 'bg-amber-100 text-amber-600' :
card.color === 'purple' ? 'bg-purple-100 text-purple-600' :
'bg-rose-100 text-rose-600'
}`}>
<Icon className="w-5 h-5" />
</div>
</div>
);
})}
</div>
);
};
+129
View File
@@ -0,0 +1,129 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { Star, MapPin, Clock, Tag, Package, Building2, AlertTriangle, Sparkles } from 'lucide-react';
export const TenderCard = ({ tender }) => {
const { toggleTrackTender, setActiveTenderDetail } = useApp();
const [day, monthYear] = tender.tarih ? tender.tarih.split('.') : ['01', '01.2026'];
const monthYearFormatted = monthYear ? `${monthYear}` : '08.2026';
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 */}
<div
onClick={() => setActiveTenderDetail(tender)}
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"
>
<span className="text-3xl font-black tracking-tight text-blue-700">{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}
</span>
</div>
{/* 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
onClick={() => setActiveTenderDetail(tender)}
className="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>
)}
</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>
</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>
<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">
<Clock className="w-3 h-3 text-emerald-600" />
<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.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>
)}
<span 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>
</div>
{/* Material Items Preview Box */}
{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>
);
};
+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>
);
};
+172
View File
@@ -0,0 +1,172 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '../context/AppContext';
import { TenderCard } from './TenderCard';
import { Download, SlidersHorizontal, Layers, ChevronLeft, ChevronRight } from 'lucide-react';
import confetti from 'canvas-confetti';
export const TenderList = () => {
const {
tenders,
searchQuery,
selectedCity,
selectedAlimTuru,
selectedSektor,
selectedKurumTipi,
quickFilter,
onlyValidOnly
} = useApp();
const [sortBy, setSortBy] = useState('newest'); // 'newest', 'urgent', 'distant'
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
// Filter logic
const filteredTenders = useMemo(() => {
return tenders.filter(tender => {
// Search Query
if (searchQuery) {
const q = searchQuery.toLowerCase();
const matchTitle = tender.baslik.toLowerCase().includes(q);
const matchKurum = tender.kurum.toLowerCase().includes(q);
const matchIkn = tender.ikn.toLowerCase().includes(q);
const matchItems = tender.items && tender.items.some(i => i.ad.toLowerCase().includes(q));
if (!matchTitle && !matchKurum && !matchIkn && !matchItems) return false;
}
// Quick Filter
if (quickFilter === 'dogrudan_temin' && tender.alimTuru !== 'Doğrudan Temin') return false;
if (quickFilter === 'bugun_eklenenler' && !(tender.eklenmeZamani.includes('önce') || tender.eklenmeZamani.includes('az'))) return false;
if (quickFilter === 'incelenenler' && !tender.incelendi) return false;
if (quickFilter === 'incelenmedi' && tender.incelendi) return false;
if (quickFilter === 'takiptekiler' && !tender.takipte) return false;
if (quickFilter === 'sozlesmeler' && tender.durum !== 'Sözleşme İmzalandı') return false;
if (quickFilter === 'sonuclananlar' && tender.durum !== 'Sonuçlandı') return false;
if (quickFilter === 'iptal' && tender.durum !== 'İptal') return false;
// Dropdown Filters
if (selectedCity && tender.sehir !== selectedCity) return false;
if (selectedAlimTuru && tender.alimTuru !== selectedAlimTuru) return false;
if (selectedSektor && tender.sektor !== selectedSektor) return false;
if (selectedKurumTipi && tender.kurumTipi !== selectedKurumTipi) return false;
return true;
}).sort((a, b) => {
if (sortBy === 'newest') return b.id.localeCompare(a.id);
if (sortBy === 'urgent') return a.tarih.localeCompare(b.tarih);
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
return 0;
});
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektor, selectedKurumTipi, quickFilter, sortBy]);
const totalPages = Math.max(1, Math.ceil(filteredTenders.length / itemsPerPage));
const paginatedTenders = filteredTenders.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
const handleExportExcel = () => {
confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } });
const headers = ['IKN', 'Baslik', 'Kurum', 'Sehir', 'Tarih', 'Alim Turu', 'Sektor'];
const rows = filteredTenders.map(t => [
t.ikn,
`"${t.baslik.replace(/"/g, '""')}"`,
`"${t.kurum.replace(/"/g, '""')}"`,
t.sehir,
t.tarih,
t.alimTuru,
t.sektor
]);
const csvContent = 'data:text/csv;charset=utf-8,\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `ihaleist_ihale_listesi_${Date.now()}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="flex-1 space-y-4">
{/* Header bar above list */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Layers className="w-5 h-5 text-blue-600" />
<h2 className="font-extrabold text-base text-slate-900">
İhale Listesi <span className="text-slate-500 font-medium text-xs">({filteredTenders.length.toLocaleString('tr-TR')} Sonuç)</span>
</h2>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="bg-slate-50 border border-slate-300 rounded-lg px-3 py-1.5 text-xs font-semibold text-slate-700 focus:ring-2 focus:ring-blue-500"
>
<option value="newest">En Yeni Eklenenler</option>
<option value="urgent">Tarihi Yaklaşanlar (Acil)</option>
<option value="distant">Tarihi Uzak Olanlar</option>
</select>
<button
onClick={handleExportExcel}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-3.5 py-2 rounded-lg flex items-center gap-1.5 transition-colors shadow-xs shrink-0"
>
<Download className="w-4 h-4" />
<span>Excel İndir</span>
</button>
</div>
</div>
{/* Tender Cards List */}
{paginatedTenders.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center shadow-xs">
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<h3 className="text-base font-bold text-slate-800">Arama kriterlerine uygun ihale bulunamadı</h3>
<p className="text-xs text-slate-500 mt-1">Lütfen arama kelimenizi değiştirin veya filtreleri temizleyin.</p>
</div>
) : (
<div className="space-y-3.5">
{paginatedTenders.map(tender => (
<TenderCard key={tender.id} tender={tender} />
))}
</div>
)}
{/* Pagination Bar */}
{totalPages > 1 && (
<div className="bg-white rounded-xl border border-slate-200 p-3 shadow-xs flex items-center justify-between text-xs font-medium text-slate-600">
<span>Sayfa {currentPage} / {totalPages} (Toplam {filteredTenders.length} Kayıt)</span>
<div className="flex items-center gap-1">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }).map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`w-8 h-8 rounded-lg font-bold transition-colors ${
currentPage === i + 1 ? 'bg-blue-600 text-white shadow-xs' : 'hover:bg-slate-100 text-slate-700'
}`}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</div>
);
};
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { useApp } from '../context/AppContext';
import { Newspaper, Trophy, CalendarDays, FileCheck2, Calculator, TrendingUp, LayoutGrid, Kanban, Radar, FolderKanban, BarChart3, Webhook, Smartphone } from 'lucide-react';
export const TopNavigation = () => {
const { activeTab, setActiveTab } = useApp();
const navItems = [
{ id: 'list', label: 'Ana İhale Listesi', icon: LayoutGrid },
{ id: 'bulten', label: 'Günlük Bülten', icon: Newspaper },
{ id: 'sonuc', label: 'Sonuç Sorgula', icon: Trophy },
{ id: 'takvim', label: 'İhale Takvimi', icon: CalendarDays },
{ id: 'sozlesme', label: 'Sözleşme Sorgula', icon: FileCheck2 },
{ id: 'kdv', label: 'KDV Hesapla', icon: Calculator },
{ id: 'fiyat', label: 'Fiyat Analizi', icon: TrendingUp },
{ id: 'kanban', label: 'Kanban Pano', icon: Kanban, badge: 'Sürükle Takip' },
{ id: 'rakip_radar', label: 'Rakip Radar', icon: Radar, badge: 'Canlı Uyar' },
{ id: 'dokuman_deposu', label: 'Doküman Deposu', icon: FolderKanban },
{ id: 'analitik_dashboard', label: 'Analitik Dashboard', icon: BarChart3 },
{ id: 'api_webhooks', label: 'API & Webhooks', icon: Webhook },
{ id: 'pwa_settings', label: 'Mobil PWA', icon: Smartphone }
];
return (
<div className="bg-white border-b border-slate-200 shadow-xs sticky top-16 z-30">
<div className="max-w-[1600px] mx-auto px-4 flex items-center gap-1 overflow-x-auto py-1 scrollbar-none">
{navItems.map(item => {
const Icon = item.icon;
const isActive = activeTab === item.id;
return (
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-all select-none ${
isActive
? 'bg-blue-50 text-blue-700 border border-blue-200 shadow-2xs'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
}`}
>
<Icon className={`w-4 h-4 ${isActive ? 'text-blue-600' : 'text-slate-400'}`} />
<span>{item.label}</span>
{item.badge && (
<span className="bg-amber-100 text-amber-800 font-bold text-[10px] px-1.5 py-0.5 rounded-full border border-amber-200">
{item.badge}
</span>
)}
</button>
);
})}
</div>
</div>
);
};
+180
View File
@@ -0,0 +1,180 @@
import React from 'react';
import { useApp } from '../../context/AppContext';
import { BarChart3, TrendingUp, Trophy, Target, MapPin, Building2, Award, ArrowUpRight, DollarSign } from 'lucide-react';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, PieChart, Pie, Cell } from 'recharts';
export const AnalyticsDashboard = () => {
const { tenders, userProfile } = useApp();
const monthlyData = [
{ month: 'Ocak', kazanim: 380000, katilim: 550000 },
{ month: 'Şubat', kazanim: 420000, katilim: 600000 },
{ month: 'Mart', kazanim: 510000, katilim: 720000 },
{ month: 'Nisan', kazanim: 490000, katilim: 680000 },
{ month: 'Mayıs', kazanim: 610000, katilim: 850000 },
{ month: 'Haziran', kazanim: 580000, katilim: 810000 },
{ month: 'Temmuz', kazanim: 410000, katilim: 590000 }
];
const pieData = [
{ name: 'Tıbbi Malzeme', value: 45, color: '#2563eb' },
{ name: 'Kırtasiye & Büro', value: 25, color: '#059669' },
{ name: 'Temizlik & Hijyen', value: 18, color: '#7c3aed' },
{ name: 'Bilişim & Donanım', value: 12, color: '#d97706' }
];
const cityHeatmap = [
{ city: 'Ankara', ihaleSayisi: 1420, ortalamaMaliyet: '₺240.000', yogunluk: 'Çok Yüksek (%88)', color: 'bg-red-500 text-white' },
{ city: 'Kayseri', ihaleSayisi: 850, ortalamaMaliyet: '₺180.000', yogunluk: 'Yüksek (%74)', color: 'bg-amber-500 text-white' },
{ city: 'İstanbul', ihaleSayisi: 2100, ortalamaMaliyet: '₺410.000', yogunluk: 'Çok Yüksek (%94)', color: 'bg-red-600 text-white' },
{ city: 'İzmir', ihaleSayisi: 980, ortalamaMaliyet: '₺210.000', yogunluk: 'Orta (%62)', color: 'bg-emerald-500 text-white' },
{ city: 'Antalya', ihaleSayisi: 640, ortalamaMaliyet: '₺165.000', yogunluk: 'Orta (%55)', color: 'bg-blue-500 text-white' }
];
return (
<div className="space-y-6 animate-fade-in">
{/* Header Banner */}
<div className="bg-linear-to-r from-slate-900 via-purple-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-purple-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-purple-600 text-white flex items-center justify-center font-black shadow-md">
<BarChart3 className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Analitik Dashboard & Fiyat Isı Haritası</h2>
<p className="text-xs text-purple-200 mt-0.5">{userProfile.company} firmasına özel kazanma oranı, ciro hedefleri ve il bazlı ihale yoğunluk analizi.</p>
</div>
</div>
</div>
{/* KPI Cards & Ciro Target */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Katıldığımız İhaleler</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-slate-900">34 İhale</span>
<div className="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center">
<Trophy className="w-4 h-4" />
</div>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">İhale Kazanma Oranımız</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-emerald-600">%68</span>
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full flex items-center gap-0.5">
<ArrowUpRight className="w-3 h-3" /> +%12
</span>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider block">Toplam Kazanılan Bedel</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-black text-indigo-900">₺3.400.000</span>
<div className="w-8 h-8 rounded-lg bg-indigo-50 text-indigo-600 flex items-center justify-center">
<Award className="w-4 h-4" />
</div>
</div>
</div>
{/* Target Progress Card */}
<div className="bg-linear-to-br from-blue-900 to-indigo-900 text-white p-5 rounded-2xl shadow-xs space-y-2">
<div className="flex justify-between items-center text-xs font-bold">
<span className="text-blue-200 uppercase tracking-wider flex items-center gap-1">
<Target className="w-3.5 h-3.5 text-amber-300" /> 2026 Ciro Hedefi
</span>
<span className="text-amber-300 font-black">%68</span>
</div>
<span className="text-xl font-black text-white block">₺3.4M / ₺5.0M</span>
<div className="w-full bg-blue-950 rounded-full h-2 overflow-hidden border border-blue-700">
<div className="bg-amber-400 h-2 rounded-full" style={{ width: '68%' }} />
</div>
</div>
</div>
{/* Recharts Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Monthly Revenue Chart */}
<div className="lg:col-span-2 bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-600" /> Aylık İhale Kazanım ve Teklif Hacmi (TL)
</h3>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={60} tickFormatter={(val) => `₺${val/1000}k`} />
<Tooltip formatter={(value) => [`₺${value.toLocaleString('tr-TR')}`, '']} />
<Bar dataKey="katilim" fill="#94a3b8" name="Toplam Verilen Teklif" radius={[4, 4, 0, 0]} />
<Bar dataKey="kazanim" fill="#2563eb" name="Kazanılan İhale Hacmi" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Sector Distribution Pie */}
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<h3 className="font-extrabold text-sm text-slate-900">Sektörel İhale Kazanım Dağılımı</h3>
<div className="h-48 w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" innerRadius={40} outerRadius={70} paddingAngle={4}>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip formatter={(value) => [`%${value}`, 'Oran']} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="space-y-1.5 pt-2">
{pieData.map(item => (
<div key={item.name} className="flex justify-between items-center text-xs font-semibold">
<span className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }}></span>
<span>{item.name}</span>
</span>
<span className="font-bold text-slate-900">%{item.value}</span>
</div>
))}
</div>
</div>
</div>
{/* City Price Heatmap Section */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<MapPin className="w-4 h-4 text-red-600" /> Şehir & Kurum Bazlı İhale Yoğunluk Isı Haritası
</h3>
<p className="text-xs text-slate-500">Bölgelere göre ihale sayıları ve ortalama idare yaklaşık maliyet seviyeleri.</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-3">
{cityHeatmap.map(item => (
<div key={item.city} className="bg-slate-50 p-4 rounded-xl border border-slate-200 space-y-2">
<div className="flex items-center justify-between">
<span className="font-extrabold text-sm text-slate-900">{item.city}</span>
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full ${item.color}`}>
{item.yogunluk}
</span>
</div>
<p className="text-xs text-slate-600 font-medium">İhale Sayısı: <strong className="text-slate-900">{item.ihaleSayisi}</strong></p>
<p className="text-xs text-slate-600 font-medium">Ort. Bedel: <strong className="text-indigo-900">{item.ortalamaMaliyet}</strong></p>
</div>
))}
</div>
</div>
</div>
);
};
+174
View File
@@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Code, Key, Webhook, Send, CheckCircle2, Copy, Plus, Lock, Globe, MessageSquare, Terminal, RefreshCw } from 'lucide-react';
import confetti from 'canvas-confetti';
export const ApiWebhooks = () => {
const { apiKeys, setApiKeys, webhooksConfig, setWebhooksConfig } = useApp();
const [copiedKey, setCopiedKey] = useState(false);
const [testSuccess, setTestSuccess] = useState(false);
const handleGenerateApiKey = () => {
const newKey = {
id: 'key-' + Date.now(),
name: 'Yeni Uygulama Anahtarı ' + (apiKeys.length + 1),
key: 'ih_live_' + Math.random().toString(36).substring(2, 18),
createdAt: new Date().toISOString().split('T')[0],
lastUsed: 'Henüz Kullanılmadı'
};
setApiKeys(prev => [newKey, ...prev]);
confetti({ particleCount: 50 });
};
const handleSendTestWebhook = () => {
setTestSuccess(true);
confetti({ particleCount: 60 });
setTimeout(() => setTestSuccess(false), 4000);
};
return (
<div className="space-y-6 animate-fade-in">
{/* Header */}
<div className="bg-linear-to-r from-slate-900 via-emerald-950 to-teal-900 text-white p-6 rounded-2xl shadow-lg border border-emerald-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-emerald-600 text-white flex items-center justify-center font-black shadow-md">
<Webhook className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">API & Webhook Entegrasyon Yönetimi</h2>
<p className="text-xs text-emerald-200 mt-0.5">ERP, SAP, Logo, Mikro muhasebe yazılımlarınız ve Slack/Teams kanallarınız ile çift yönlü canlı veri aktarımı.</p>
</div>
</div>
</div>
{/* REST API Keys Section */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div>
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<Key className="w-4 h-4 text-emerald-600" /> REST API Canlı Erişim Anahtarları
</h3>
<p className="text-xs text-slate-500">Geliştiricileriniz için RESTful JSON API anahtarları.</p>
</div>
<button
onClick={handleGenerateApiKey}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-extrabold text-xs px-4 py-2 rounded-xl flex items-center gap-1.5 transition-colors shadow-xs"
>
<Plus className="w-4 h-4" />
<span>Yeni API Anahtarı Üret</span>
</button>
</div>
<div className="space-y-3">
{apiKeys.map(k => (
<div key={k.id} className="bg-slate-50 p-4 rounded-xl border border-slate-200 flex flex-col md:flex-row md:items-center justify-between gap-3">
<div className="space-y-1">
<span className="font-bold text-xs text-slate-900">{k.name}</span>
<div className="flex items-center gap-2">
<code className="bg-slate-900 text-amber-400 font-mono text-xs px-3 py-1 rounded-lg">
{k.key}
</code>
<button
onClick={() => {
navigator.clipboard.writeText(k.key);
setCopiedKey(true);
setTimeout(() => setCopiedKey(false), 2000);
}}
className="p-1.5 text-slate-500 hover:text-slate-900 hover:bg-slate-200 rounded-lg transition-colors text-xs font-bold flex items-center gap-1"
>
<Copy className="w-3.5 h-3.5" />
<span>{copiedKey ? 'Kopyalandı!' : 'Kopyala'}</span>
</button>
</div>
</div>
<div className="text-[11px] text-slate-500 font-medium space-y-0.5">
<p>Oluşturulma: {k.createdAt}</p>
<p>Son Kullanım: <strong className="text-emerald-700">{k.lastUsed}</strong></p>
</div>
</div>
))}
</div>
</div>
{/* Webhook Configuration & Slack / Teams Section */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-6">
<div className="border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-blue-600" /> Slack & Microsoft Teams Webhook Ayarları
</h3>
<p className="text-xs text-slate-500">İhale etkinlikleri gerçekleştiğinde anında bildirim alacağınız HTTP Webhook URL adresleri.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Slack Webhook URL</label>
<input
type="url"
value={webhooksConfig.slackWebhookUrl}
onChange={(e) => setWebhooksConfig({ ...webhooksConfig, slackWebhookUrl: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-xs font-mono text-slate-800"
/>
</div>
<div className="space-y-2">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Microsoft Teams Webhook URL</label>
<input
type="url"
value={webhooksConfig.teamsWebhookUrl}
onChange={(e) => setWebhooksConfig({ ...webhooksConfig, teamsWebhookUrl: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-xs font-mono text-slate-800"
/>
</div>
</div>
{/* Event Trigger Checkboxes */}
<div className="space-y-3 pt-2">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Tetiklenecek Webhook Etkinlikleri</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{[
{ key: 'newTenderMatch', title: '🟢 Yeni Uyumlu İhale Yayınlandığında' },
{ key: 'tenderWon', title: '🏆 İhale Kazanıldığında ERP/Muhasebeye Gönder' },
{ key: 'bidDeadlineAlert', title: '⚠️ İhale Son Başvurusuna 3 Saat Kala Uyar' },
{ key: 'competitorActivity', title: '🛡️ İzlenen Rakip Firma Teklif Verince Bildir' }
].map(item => (
<label key={item.key} 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-xs text-slate-800">
<input
type="checkbox"
checked={webhooksConfig.events[item.key]}
onChange={(e) => setWebhooksConfig({
...webhooksConfig,
events: { ...webhooksConfig.events, [item.key]: e.target.checked }
})}
className="w-4 h-4 accent-emerald-600 rounded"
/>
<span>{item.title}</span>
</label>
))}
</div>
</div>
{/* Test Webhook Button */}
<div className="pt-2 flex items-center gap-3">
<button
onClick={handleSendTestWebhook}
className="bg-slate-900 hover:bg-slate-800 text-white font-extrabold text-xs px-6 py-3 rounded-xl flex items-center gap-2 shadow-md transition-colors"
>
<Send className="w-4 h-4 text-emerald-400" />
<span>Test Webhook Payload Gönder (HTTP 200 OK)</span>
</button>
{testSuccess && (
<span className="text-xs font-extrabold text-emerald-700 bg-emerald-50 border border-emerald-200 px-3 py-1.5 rounded-xl flex items-center gap-1.5 animate-fade-in">
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span>Test Webhook Başarıyla Slack & Teams Kanallarına İletildi! (HTTP 200 OK)</span>
</span>
)}
</div>
</div>
</div>
);
};
+174
View File
@@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Radar, Bell, Plus, ShieldAlert, Award, FileText, CheckCircle2, Trash2, ArrowRight } from 'lucide-react';
import confetti from 'canvas-confetti';
export const CompetitorRadar = () => {
const { competitorWatchlist, setCompetitorWatchlist, setActiveTab } = useApp();
const [newCompetitorName, setNewCompetitorName] = useState('');
const [newSector, setNewSector] = useState('Medikal & Tıbbi Malzeme');
const handleAddCompetitor = (e) => {
e.preventDefault();
if (!newCompetitorName.trim()) return;
const newComp = {
id: 'cw-' + Date.now(),
name: newCompetitorName,
sector: newSector,
winCount: 0,
totalRevenue: 0,
marketShare: 5,
status: 'Aktif İzleniyor',
alertOnBid: true,
alertOnWin: true
};
setCompetitorWatchlist(prev => [newComp, ...prev]);
setNewCompetitorName('');
confetti({ particleCount: 50 });
};
const toggleAlert = (id, field) => {
setCompetitorWatchlist(prev => prev.map(c => c.id === id ? { ...c, [field]: !c[field] } : c));
};
const removeCompetitor = (id) => {
setCompetitorWatchlist(prev => prev.filter(c => c.id !== id));
};
return (
<div className="space-y-6 animate-fade-in">
{/* Header Banner */}
<div className="bg-linear-to-r from-slate-900 via-indigo-950 to-blue-900 text-white p-6 rounded-2xl shadow-lg border border-indigo-800/40 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center font-black shadow-md">
<Radar className="w-6 h-6 animate-pulse" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="font-extrabold text-xl">Rakip Radar & Proaktif İhale Uyarısı</h2>
<span className="bg-indigo-500 text-white text-[10px] font-extrabold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
Proaktif Takip
</span>
</div>
<p className="text-xs text-indigo-200 mt-0.5">İzlemeye aldığınız rakip firmalar ihaleye teklif verdiğinde veya kazandığında anında SMS ve Push bildirimi alın.</p>
</div>
</div>
<button
onClick={() => setActiveTab('sozlesme')}
className="bg-white/10 hover:bg-white/20 text-white font-bold text-xs px-4 py-2.5 rounded-xl border border-white/20 transition-colors flex items-center gap-2 shrink-0"
>
<FileText className="w-4 h-4 text-amber-400" />
<span>Sözleşme Sorgulama İncele</span>
<ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
{/* Add New Competitor Form */}
<form onSubmit={handleAddCompetitor} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs flex flex-col md:flex-row gap-3 items-end">
<div className="flex-1 space-y-1.5 w-full">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">İzlenecek Rakip Firma Adı</label>
<input
type="text"
required
value={newCompetitorName}
onChange={(e) => setNewCompetitorName(e.target.value)}
placeholder="Örn: ABC Medikal Sanayi ve Ticaret A.Ş."
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-xs font-semibold text-slate-900 focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div className="w-full md:w-64 space-y-1.5">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Sektör</label>
<select
value={newSector}
onChange={(e) => setNewSector(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-xs font-semibold text-slate-900"
>
<option>Medikal & Tıbbi Malzeme</option>
<option>Sağlık & Büro Malzemeleri</option>
<option>Laboratuvar & Bilişim</option>
<option>Kırtasiye & Sanatsal</option>
<option>Temizlik & Hijyen</option>
</select>
</div>
<button
type="submit"
className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-500 text-white font-extrabold text-xs px-6 py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors shrink-0"
>
<Plus className="w-4 h-4" />
<span>Radara Ekle</span>
</button>
</form>
{/* Tracked Competitors Table */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-indigo-600" /> Takipteki Rakip Firmalar ({competitorWatchlist.length})
</h3>
<span className="text-xs text-slate-500">Otomatik bildirimler aktif</span>
</div>
<div className="divide-y divide-slate-100">
{competitorWatchlist.map((comp) => (
<div key={comp.id} className="p-5 hover:bg-slate-50/80 transition-colors flex flex-col md:flex-row md:items-center justify-between gap-4">
{/* Info */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="font-bold text-sm text-slate-900">{comp.name}</h4>
<span className="bg-indigo-100 text-indigo-800 font-bold text-[10px] px-2 py-0.5 rounded-full">
{comp.sector}
</span>
</div>
<div className="flex items-center gap-4 text-xs text-slate-500 font-medium">
<span>🏆 Kazandığı İhale: <strong className="text-slate-800">{comp.winCount}</strong></span>
<span>💰 Toplam Hacim: <strong className="text-emerald-700">₺{(comp.totalRevenue / 1000000).toFixed(1)}M</strong></span>
<span>📊 Tahmini Pazar Payı: <strong className="text-indigo-900">%{comp.marketShare}</strong></span>
</div>
</div>
{/* Notification Toggles & Actions */}
<div className="flex items-center gap-3 shrink-0">
<button
onClick={() => toggleAlert(comp.id, 'alertOnBid')}
className={`text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-colors ${
comp.alertOnBid ? 'bg-amber-50 text-amber-800 border-amber-300' : 'bg-slate-100 text-slate-400 border-slate-200'
}`}
>
<Bell className="w-3.5 h-3.5" />
<span>Teklif Verince Uyar</span>
</button>
<button
onClick={() => toggleAlert(comp.id, 'alertOnWin')}
className={`text-xs font-extrabold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-colors ${
comp.alertOnWin ? 'bg-emerald-50 text-emerald-800 border-emerald-300' : 'bg-slate-100 text-slate-400 border-slate-200'
}`}
>
<Award className="w-3.5 h-3.5" />
<span>Kazanırsa Uyar</span>
</button>
<button
onClick={() => removeCompetitor(comp.id)}
className="p-2 text-slate-400 hover:text-red-600 rounded-lg transition-colors"
title="Radardan Çıkar"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
</div>
</div>
);
};
+78
View File
@@ -0,0 +1,78 @@
import React, { useState } from 'react';
import { COMPETITOR_DATA } from '../../data/mockTenders';
import { FileCheck2, Search, Building2, TrendingUp, Award, Shield } from 'lucide-react';
export const ContractSearch = () => {
const [searchTerm, setSearchTerm] = useState('');
const filteredCompetitors = COMPETITOR_DATA.filter(c =>
c.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="space-y-4 my-4 animate-fade-in">
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-indigo-600 text-white flex items-center justify-center shadow-md">
<FileCheck2 className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Sözleşme Bedeli & Rakip Analizi</h1>
<p className="text-xs text-slate-500">Rakip firmaların kamu ihalelerinden aldığı toplam iş hacmini ve sözleşme bedellerini sorgulayın.</p>
</div>
</div>
<div className="relative w-full md:w-80">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Firma adı ile arayın..."
className="w-full bg-slate-50 border border-slate-300 rounded-lg pl-9 pr-4 py-2 text-xs font-semibold"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
</div>
</div>
{/* Competitor Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredCompetitors.map((comp, idx) => (
<div key={idx} className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs hover:shadow-md transition-all space-y-4">
<div className="flex items-start justify-between border-b border-slate-100 pb-3">
<div>
<span className="text-[10px] font-extrabold bg-indigo-100 text-indigo-800 px-2 py-0.5 rounded">
Sektör Sırası #{idx + 1}
</span>
<h3 className="font-extrabold text-base text-slate-900 mt-1">{comp.name}</h3>
</div>
<Award className="w-6 h-6 text-indigo-600 shrink-0" />
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<span className="text-slate-400 block font-medium">Toplam İş Hacmi</span>
<span className="text-sm font-black text-indigo-900 mt-0.5 block">{comp.totalVolume}</span>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<span className="text-slate-400 block font-medium">Kazandığı İhale</span>
<span className="text-sm font-black text-emerald-700 mt-0.5 block">{comp.winCount} Adet İhale</span>
</div>
</div>
<div>
<div className="flex justify-between text-xs font-semibold text-slate-700 mb-1">
<span>Tahmini Pazar Payı</span>
<span className="font-bold text-indigo-700">%{comp.share}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
<div className="bg-indigo-600 h-2 rounded-full" style={{ width: `${comp.share}%` }}></div>
</div>
</div>
</div>
))}
</div>
</div>
);
};
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { useApp } from '../../context/AppContext';
import { Newspaper, Calendar, Building2, Package, ArrowRight } from 'lucide-react';
export const DailyBulletin = () => {
const { tenders, setActiveTenderDetail } = useApp();
const todayStr = '10.08.2026';
const todayTenders = tenders.filter(t => t.tarih === todayStr || t.eklenmeZamani.includes('az') || t.eklenmeZamani.includes('önce'));
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Header */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-600 text-white flex items-center justify-center shadow-md">
<Newspaper className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Günlük İhale Bülteni</h1>
<p className="text-xs text-slate-500">{todayStr} Tarihinde Yayınlanan Resmi Doğrudan Temin ve Kamu İhaleleri</p>
</div>
</div>
<span className="bg-blue-50 text-blue-800 font-extrabold text-xs px-3 py-1.5 rounded-full border border-blue-200">
{todayTenders.length} Bugünlük İhale Kaydı
</span>
</div>
{/* Bulletin Group List */}
<div className="space-y-4">
{todayTenders.map(t => (
<div key={t.id} className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs hover:shadow-md transition-all space-y-3">
<div className="flex items-start justify-between border-b border-slate-100 pb-3">
<div>
<span className="text-[10px] font-extrabold bg-blue-100 text-blue-800 px-2 py-0.5 rounded">
#{t.ikn}
</span>
<h3
onClick={() => setActiveTenderDetail(t)}
className="font-bold text-base text-slate-900 hover:text-blue-600 cursor-pointer mt-1"
>
{t.baslik}
</h3>
</div>
<span className="text-xs font-bold text-slate-500 bg-slate-100 px-2.5 py-1 rounded-md">
⏰ {t.saat}
</span>
</div>
<div className="flex items-center gap-4 text-xs text-slate-600">
<span className="flex items-center gap-1 font-semibold">
<Building2 className="w-4 h-4 text-slate-400" /> {t.kurum}
</span>
<span className="font-bold text-blue-700">📍 {t.sehir}</span>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200 text-xs space-y-1">
<span className="font-bold text-slate-700 block mb-1">Bülten Kalem Özeti:</span>
{t.items?.map((item, i) => (
<div key={i} className="flex justify-between font-semibold text-slate-700">
<span>📦 {item.ad}</span>
<span className="font-mono text-slate-900">{item.miktar} {item.birim}</span>
</div>
))}
</div>
<button
onClick={() => setActiveTenderDetail(t)}
className="text-xs font-bold text-blue-600 hover:text-blue-800 flex items-center gap-1 hover:underline"
>
<span>İhale Detayını Göster</span>
<ArrowRight className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
);
};
+147
View File
@@ -0,0 +1,147 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { FolderKanban, FileText, Upload, Plus, ShieldCheck, AlertCircle, CheckCircle2, Download, Trash2, Tag, Calendar } from 'lucide-react';
import confetti from 'canvas-confetti';
export const DocumentVault = () => {
const { vaultDocuments, setVaultDocuments, userProfile } = useApp();
const [docName, setDocName] = useState('');
const [category, setCategory] = useState('Kalite Belgeleri');
const handleAddDocument = (e) => {
e.preventDefault();
if (!docName.trim()) return;
const newDoc = {
id: 'doc-' + Date.now(),
name: docName.endsWith('.pdf') ? docName : docName + '.pdf',
category,
size: (Math.random() * 2 + 0.5).toFixed(1) + ' MB',
updatedAt: new Date().toISOString().split('T')[0],
expiresAt: '2027-12-31',
status: 'Geçerli'
};
setVaultDocuments(prev => [newDoc, ...prev]);
setDocName('');
confetti({ particleCount: 60 });
};
const deleteDoc = (id) => {
setVaultDocuments(prev => prev.filter(d => d.id !== id));
};
return (
<div className="space-y-6 animate-fade-in">
{/* Header */}
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-blue-800/40 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-blue-600 text-white flex items-center justify-center font-black shadow-md">
<FolderKanban className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="font-extrabold text-xl">Doküman ve Teklif Deposu</h2>
<span className="bg-emerald-500 text-slate-950 text-[10px] font-extrabold px-2.5 py-0.5 rounded-full uppercase tracking-wider">
Resmi Evrak Kütüphanesi
</span>
</div>
<p className="text-xs text-blue-200 mt-0.5">Sık kullanılan resmi belgelerinizi (İmza sirküleri, ISO, ÜTS, Faaliyet Belgesi) tek yerde saklayın; ihale teklif paketine saniyeler içinde ekleyin.</p>
</div>
</div>
</div>
{/* Add Document Form */}
<form onSubmit={handleAddDocument} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs flex flex-col md:flex-row gap-3 items-end">
<div className="flex-1 space-y-1.5 w-full">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Yeni Doküman Adı</label>
<input
type="text"
required
value={docName}
onChange={(e) => setDocName(e.target.value)}
placeholder="Örn: 2026 ISO 9001 Kalite Belgesi.pdf"
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-xs font-semibold text-slate-900 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="w-full md:w-64 space-y-1.5">
<label className="block text-xs font-bold text-slate-800 uppercase tracking-wider">Kategori</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-xs font-semibold text-slate-900"
>
<option>Yasal Belgeler</option>
<option>Kalite Belgeleri</option>
<option>Tıbbi Cihaz Kaydı</option>
<option>Resmi Kayıtlar</option>
<option>Mali Belgeler</option>
</select>
</div>
<button
type="submit"
className="w-full md:w-auto bg-blue-600 hover:bg-blue-500 text-white font-extrabold text-xs px-6 py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors shrink-0"
>
<Upload className="w-4 h-4" />
<span>Depoya Ekle</span>
</button>
</form>
{/* Documents Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{vaultDocuments.map((doc) => (
<div key={doc.id} className="bg-white p-5 rounded-2xl border border-slate-200 shadow-xs hover:shadow-md transition-all space-y-3 flex flex-col justify-between">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="bg-slate-100 text-slate-700 font-bold text-[10px] px-2.5 py-1 rounded-lg border border-slate-200 flex items-center gap-1">
<Tag className="w-3 h-3 text-blue-600" /> {doc.category}
</span>
<span className="text-[10px] font-bold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded-full flex items-center gap-1">
<ShieldCheck className="w-3 h-3" /> {doc.status}
</span>
</div>
<div className="flex items-start gap-3 pt-1">
<div className="w-10 h-10 rounded-xl bg-red-50 text-red-600 border border-red-200 flex items-center justify-center shrink-0 font-black">
<FileText className="w-5 h-5" />
</div>
<div>
<h4 className="font-bold text-xs text-slate-900 leading-snug line-clamp-2">{doc.name}</h4>
<p className="text-[10px] text-slate-400 mt-0.5">Boyut: {doc.size}</p>
</div>
</div>
</div>
<div className="border-t border-slate-100 pt-3 flex items-center justify-between text-[11px] text-slate-500 font-medium">
<span className="flex items-center gap-1 text-slate-600">
<Calendar className="w-3.5 h-3.5 text-slate-400" /> Son Geçerlilik: <strong>{doc.expiresAt}</strong>
</span>
<div className="flex items-center gap-1">
<button
onClick={() => alert(`${doc.name} başarıyla indirildi.`)}
className="p-1.5 text-slate-600 hover:text-blue-600 rounded-lg hover:bg-slate-100 transition-colors"
title="İndir"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={() => deleteDoc(doc.id)}
className="p-1.5 text-slate-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
};
+130
View File
@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
import { TrendingUp, Search, Download, Sparkles, AlertCircle } from 'lucide-react';
import confetti from 'canvas-confetti';
export const PriceAnalysis = () => {
const [selectedProduct, setSelectedProduct] = useState('35*50 TUVAL');
const priceHistoryData = [
{ month: 'Ocak 2026', minFiyat: 110, ortFiyat: 125, maksFiyat: 140 },
{ month: 'Şubat 2026', minFiyat: 115, ortFiyat: 130, maksFiyat: 145 },
{ month: 'Mart 2026', minFiyat: 120, ortFiyat: 135, maksFiyat: 155 },
{ month: 'Nisan 2026', minFiyat: 125, ortFiyat: 140, maksFiyat: 160 },
{ month: 'Mayıs 2026', minFiyat: 130, ortFiyat: 145, maksFiyat: 170 },
{ month: 'Haziran 2026', minFiyat: 135, ortFiyat: 150, maksFiyat: 180 },
{ month: 'Temmuz 2026', minFiyat: 140, ortFiyat: 155, maksFiyat: 185 },
{ month: 'Ağustos 2026', minFiyat: 145, ortFiyat: 160, maksFiyat: 190 }
];
const productsList = [
'35*50 TUVAL',
'50*70 TUVAL',
'1 Gözlü Yemek Kabı',
'Kablosuz Panik Butonu',
'G4 Panel Filtre',
'Çamaşır Suyu 20 L',
'Revizyon Kalça Protez Femoral Stem'
];
const handleDownloadReport = () => {
confetti({ particleCount: 70 });
alert(`${selectedProduct} Fiyat Trend Analizi Raporu (PDF/CSV) İndirildi.`);
};
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Top Banner */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-emerald-600 text-white flex items-center justify-center shadow-md">
<TrendingUp className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Birim Fiyat Analizi & Trend Grafikleri</h1>
<p className="text-xs text-slate-500">Ürün bazında geçmiş ihalelerdeki birim fiyat trendini ve ortalama teklif marjlarını inceleyin.</p>
</div>
</div>
<button
onClick={handleDownloadReport}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-4 py-2 rounded-lg flex items-center gap-2 shadow-xs shrink-0"
>
<Download className="w-4 h-4" /> Rapor İndir (PDF/CSV)
</button>
</div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column: Product Selector */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-4 h-fit">
<h3 className="font-bold text-xs uppercase tracking-wider text-slate-800 border-b border-slate-100 pb-2">
Ürün / Malzeme Seçimi
</h3>
<div className="space-y-1.5">
{productsList.map(prod => (
<button
key={prod}
onClick={() => setSelectedProduct(prod)}
className={`w-full text-left p-3 rounded-lg text-xs font-semibold transition-all ${
selectedProduct === prod
? 'bg-emerald-600 text-white shadow-xs'
: 'bg-slate-50 hover:bg-slate-100 text-slate-700 border border-slate-200'
}`}
>
📦 {prod}
</button>
))}
</div>
</div>
{/* Right 2 Columns: Recharts Chart & AI Assistant */}
<div className="lg:col-span-2 space-y-6">
{/* Recharts Line Chart */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-bold text-sm text-slate-900">
{selectedProduct} — 8 Aylık Birim Fiyat Değişim Grafiği (TL)
</h3>
<span className="text-[10px] bg-emerald-100 text-emerald-800 font-extrabold px-2 py-0.5 rounded-full">
Canlı EKAP Verisi
</span>
</div>
<div className="h-72 w-full pt-2">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={priceHistoryData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Line type="monotone" dataKey="ortFiyat" stroke="#059669" strokeWidth={3} name="Ortalama Fiyat" />
<Line type="monotone" dataKey="minFiyat" stroke="#2563eb" strokeWidth={2} strokeDasharray="5 5" name="En Düşük Fiyat" />
<Line type="monotone" dataKey="maksFiyat" stroke="#dc2626" strokeWidth={2} strokeDasharray="5 5" name="En Yüksek Fiyat" />
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* AI Pricing Advice Box */}
<div className="bg-linear-to-r from-slate-900 to-emerald-950 text-white p-5 rounded-xl space-y-3 shadow-md border border-emerald-900/40">
<div className="flex items-center gap-2 font-bold text-sm text-emerald-400">
<Sparkles className="w-5 h-5 text-amber-400" />
<span>Yapay Zeka Teklif Öneri Motoru</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
<strong>{selectedProduct}</strong> için son 8 aydaki ihale kazanma verilerine göre optimal teklif birim fiyatınız: <strong className="text-amber-300 text-sm">₺155 - ₺165</strong> aralığıdır. Bu aralıkta kazanma olasılığınız <strong>%88</strong> olarak hesaplanmıştır.
</p>
</div>
</div>
</div>
</div>
);
};
+134
View File
@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Smartphone, Wifi, WifiOff, Download, Bell, CheckCircle2, ShieldCheck, Zap, Sparkles } from 'lucide-react';
import confetti from 'canvas-confetti';
export const PwaSettings = () => {
const { userProfile } = useApp();
const [offlineMode, setOfflineMode] = useState(true);
const [pushSubscribed, setPushSubscribed] = useState(true);
const [installed, setInstalled] = useState(false);
const handleInstallPwa = () => {
setInstalled(true);
confetti({ particleCount: 70 });
};
return (
<div className="space-y-6 animate-fade-in">
{/* Header Banner */}
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-cyan-900 text-white p-6 rounded-2xl shadow-lg border border-cyan-800/40 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-cyan-600 text-white flex items-center justify-center font-black shadow-md">
<Smartphone className="w-6 h-6" />
</div>
<div>
<h2 className="font-extrabold text-xl">Mobil PWA & Saha Ekibi Offline Modu</h2>
<p className="text-xs text-cyan-200 mt-0.5">Saha satış temsilcileriniz için internet bağlantısı olmadığında dahi çevrimdışı çalışma ve anlık push bildirimi.</p>
</div>
</div>
</div>
{/* Main Controls Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Offline Cache Status */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
{offlineMode ? <Wifi className="w-4 h-4 text-emerald-600" /> : <WifiOff className="w-4 h-4 text-slate-400" />} Çevrimdışı (Offline) Önbellek
</h3>
<span className="bg-emerald-100 text-emerald-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
Senkronize
</span>
</div>
<div className="space-y-2 text-xs text-slate-700">
<p className="flex justify-between">
<span>İhale Kayıtları:</span>
<strong className="text-slate-900">102 İhale Önbellekte</strong>
</p>
<p className="flex justify-between">
<span>Şartname PDF'leri:</span>
<strong className="text-slate-900">18 Belge Çevrimdışı</strong>
</p>
<p className="flex justify-between">
<span>Son Senkronizasyon:</span>
<strong className="text-emerald-700">Bugün 14:45</strong>
</p>
</div>
<button
onClick={() => setOfflineMode(!offlineMode)}
className="w-full bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs py-2.5 rounded-xl border border-slate-200 transition-colors"
>
{offlineMode ? 'Çevrimdışı Önbelleği Yenile' : 'Çevrimdışı Modu Aktif Et'}
</button>
</div>
{/* Mobile Push Notifications */}
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<h3 className="font-extrabold text-sm text-slate-900 flex items-center gap-2">
<Bell className="w-4 h-4 text-amber-500" /> Anlık Mobil Push Bildirimleri
</h3>
<span className="bg-amber-100 text-amber-800 font-extrabold text-[10px] px-2 py-0.5 rounded-full">
İzin Verildi
</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed">
İhale son başvuru saatine 3 saat kala mobil cihazınıza sesli ve titreşimli anlık uyarı bildirimi gönderilir.
</p>
<label className="flex items-center justify-between p-3 bg-slate-50 rounded-xl border border-slate-200 cursor-pointer text-xs font-bold text-slate-800">
<span>Mobil Push Uyarısı</span>
<input
type="checkbox"
checked={pushSubscribed}
onChange={() => setPushSubscribed(!pushSubscribed)}
className="w-4 h-4 accent-cyan-600 rounded"
/>
</label>
</div>
{/* Install PWA Application */}
<div className="bg-linear-to-br from-slate-900 to-cyan-950 text-white p-6 rounded-2xl shadow-xs space-y-4 border border-cyan-800/40">
<div className="flex items-center justify-between">
<h3 className="font-black text-sm text-white flex items-center gap-2">
<Download className="w-4 h-4 text-cyan-400" /> İhaleist Mobil / Masaüstü
</h3>
<span className="bg-cyan-500 text-slate-950 font-black text-[10px] px-2 py-0.5 rounded-full">
PWA 2.0
</span>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
İhaleist'i iOS, Android, Windows veya Mac cihazınıza bir tıklama ile uygulama olarak kurun.
</p>
<button
onClick={handleInstallPwa}
disabled={installed}
className="w-full bg-cyan-500 hover:bg-cyan-400 disabled:bg-slate-700 text-slate-950 font-black text-xs py-3 rounded-xl flex items-center justify-center gap-2 shadow-md transition-colors cursor-pointer"
>
{installed ? (
<>
<CheckCircle2 className="w-4 h-4 text-emerald-950" />
<span>Cihaza Yüklendi!</span>
</>
) : (
<>
<Download className="w-4 h-4 text-slate-950" />
<span>İhaleist Uygulamasını Cihaza Yükle</span>
</>
)}
</button>
</div>
</div>
</div>
);
};
+77
View File
@@ -0,0 +1,77 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { Trophy, Search, Building2, CheckCircle2, Award } from 'lucide-react';
export const ResultSearch = () => {
const { tenders } = useApp();
const [query, setQuery] = useState('');
const completedTenders = tenders.filter(t => t.durum === 'Sonuçlandı' || t.kazananFirma);
const filteredResults = completedTenders.filter(t => {
if (!query) return true;
const q = query.toLowerCase();
return (
t.baslik.toLowerCase().includes(q) ||
t.ikn.toLowerCase().includes(q) ||
t.kurum.toLowerCase().includes(q) ||
(t.kazananFirma && t.kazananFirma.toLowerCase().includes(q))
);
});
return (
<div className="space-y-4 my-4 animate-fade-in">
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-amber-500 text-white flex items-center justify-center shadow-md">
<Trophy className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">İhale Sonuç Sorgulama</h1>
<p className="text-xs text-slate-500">Sonuçlanmış ihaleleri, kazanan firmaları ve sözleşme bedellerini sorgulayın.</p>
</div>
</div>
<div className="relative w-full md:w-80">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="İKN, kurum veya kazanan firma ara..."
className="w-full bg-slate-50 border border-slate-300 rounded-lg pl-9 pr-4 py-2 text-xs font-semibold"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
</div>
</div>
<div className="space-y-3">
{filteredResults.map(t => (
<div key={t.id} className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex flex-col md:flex-row items-center justify-between gap-4">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<span className="text-[10px] font-extrabold bg-amber-100 text-amber-900 px-2 py-0.5 rounded">
#{t.ikn}
</span>
<span className="text-xs text-slate-500 font-semibold">{t.tarih}</span>
</div>
<h3 className="font-bold text-sm text-slate-900">{t.baslik}</h3>
<p className="text-xs text-slate-500 flex items-center gap-1">
<Building2 className="w-3.5 h-3.5 text-slate-400" /> {t.kurum} ({t.sehir})
</p>
</div>
<div className="bg-emerald-50 border border-emerald-200 p-3 rounded-xl text-right shrink-0 w-full md:w-64">
<span className="text-[10px] font-bold text-emerald-700 block uppercase tracking-wider">KAZANAN FİRMA</span>
<p className="font-extrabold text-xs text-emerald-950 truncate">{t.kazananFirma || 'Tanrıverdi Medikal Ltd.'}</p>
<p className="font-black text-sm text-emerald-700 mt-0.5">
₺{(t.sozlesmeBedeli || 57400).toLocaleString('tr-TR')}
</p>
</div>
</div>
))}
</div>
</div>
);
};
+87
View File
@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import { useApp } from '../../context/AppContext';
import { CalendarDays, Clock, MapPin, AlertCircle } from 'lucide-react';
export const TenderCalendar = () => {
const { tenders, setActiveTenderDetail } = useApp();
const [selectedDay, setSelectedDay] = useState('10.08.2026');
const daysList = [
{ date: '10.08.2026', label: 'Bugün (Pazartesi)', count: 3 },
{ date: '11.08.2026', label: 'Salı', count: 4 },
{ date: '12.08.2026', label: 'Çarşamba', count: 2 },
{ date: '13.08.2026', label: 'Perşembe', count: 5 },
{ date: '14.08.2026', label: 'Cuma', count: 3 },
{ date: '15.08.2026', label: 'Cumartesi', count: 1 }
];
const dayTenders = tenders.filter(t => t.tarih === selectedDay || (selectedDay === '10.08.2026' && t.tarih.includes('10')));
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Top Banner */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-600 text-white flex items-center justify-center shadow-md">
<CalendarDays className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">7 Günlük İhale Takvimi</h1>
<p className="text-xs text-slate-500">Önümüzdeki 7 gün içerisinde kapanacak ihaleleri günü gününe takip edin.</p>
</div>
</div>
</div>
{/* Day Selector Buttons */}
<div className="flex items-center gap-2 overflow-x-auto pb-1">
{daysList.map(d => (
<button
key={d.date}
onClick={() => setSelectedDay(d.date)}
className={`flex-1 min-w-[130px] p-3 rounded-xl border text-center transition-all cursor-pointer ${
selectedDay === d.date
? 'bg-purple-600 text-white border-purple-700 shadow-sm font-bold'
: 'bg-white text-slate-700 border-slate-200 hover:bg-slate-50 font-semibold'
}`}
>
<span className="text-[11px] block text-purple-200 opacity-90">{d.label}</span>
<span className="text-sm font-black block mt-0.5">{d.date}</span>
<span className={`text-[10px] mt-1 inline-block px-2 py-0.5 rounded-full ${
selectedDay === d.date ? 'bg-white/20 text-white font-extrabold' : 'bg-purple-100 text-purple-800 font-bold'
}`}>
{d.count} İhale Kapanıyor
</span>
</button>
))}
</div>
{/* Tenders List for Selected Day */}
<div className="space-y-3">
{dayTenders.map(t => (
<div
key={t.id}
onClick={() => setActiveTenderDetail(t)}
className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs hover:shadow-md cursor-pointer transition-all flex items-center justify-between gap-4"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="bg-purple-100 text-purple-900 text-[10px] font-extrabold px-2 py-0.5 rounded">
⏰ {t.saat}
</span>
<span className="text-xs text-slate-400 font-mono">#{t.ikn}</span>
</div>
<h3 className="font-bold text-sm text-slate-900 hover:text-purple-600">{t.baslik}</h3>
<p className="text-xs text-slate-500 font-medium">🏢 {t.kurum} • 📍 {t.sehir}</p>
</div>
<span className="bg-amber-500 text-white font-extrabold text-xs px-3 py-1.5 rounded-lg shrink-0">
{t.alimTuru}
</span>
</div>
))}
</div>
</div>
);
};
+177
View File
@@ -0,0 +1,177 @@
import React, { useState } from 'react';
import { Calculator, Copy, Check } from 'lucide-react';
export const VatCalculator = () => {
const [mode, setMode] = useState('haric'); // 'haric' (net to gross) or 'dahil' (gross to net)
const [amount, setAmount] = useState(100000);
const [vatRate, setVatRate] = useState(20); // 1, 10, 20
const [tevkifatRate, setTevkifatRate] = useState(0); // 0, 50 (5/10), 70 (7/10), 90 (9/10)
const [copied, setCopied] = useState(false);
// Calculations
let netAmount = 0;
let vatAmount = 0;
let grossAmount = 0;
let tevkifatAmount = 0;
let payableVat = 0;
if (mode === 'haric') {
netAmount = Number(amount) || 0;
vatAmount = netAmount * (vatRate / 100);
grossAmount = netAmount + vatAmount;
} else {
grossAmount = Number(amount) || 0;
netAmount = grossAmount / (1 + vatRate / 100);
vatAmount = grossAmount - netAmount;
}
if (tevkifatRate > 0) {
tevkifatAmount = vatAmount * (tevkifatRate / 100);
payableVat = vatAmount - tevkifatAmount;
} else {
payableVat = vatAmount;
}
const handleCopy = () => {
const summary = `Matrah: ₺${netAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })} | KDV (%${vatRate}): ₺${vatAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })} | Genel Toplam: ₺${grossAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}`;
navigator.clipboard.writeText(summary);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="max-w-3xl mx-auto my-4 space-y-4 animate-fade-in">
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-xs flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-600 text-white flex items-center justify-center shadow-md">
<Calculator className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">İhale KDV & Tevkifat Hesaplayıcı</h1>
<p className="text-xs text-slate-500">Teklif matrahından KDV dahil tutarı veya KDV dahilden net matrahı hesaplayın.</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-xs space-y-6">
{/* Mode Selector */}
<div className="grid grid-cols-2 gap-3 p-1 bg-slate-100 rounded-xl">
<button
onClick={() => setMode('haric')}
className={`py-2.5 rounded-lg text-xs font-extrabold transition-all ${
mode === 'haric' ? 'bg-white text-blue-700 shadow-2xs' : 'text-slate-600 hover:text-slate-900'
}`}
>
KDV Hariç ➔ KDV Dahil Hesapla
</button>
<button
onClick={() => setMode('dahil')}
className={`py-2.5 rounded-lg text-xs font-extrabold transition-all ${
mode === 'dahil' ? 'bg-white text-blue-700 shadow-2xs' : 'text-slate-600 hover:text-slate-900'
}`}
>
KDV Dahil ➔ Matrah Hesapla
</button>
</div>
{/* Inputs */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div>
<label className="block font-bold text-slate-700 mb-1.5">
{mode === 'haric' ? 'Teklif Tutarı (KDV Hariç Matrah)' : 'KDV Dahil Toplam Tutar'}
</label>
<div className="relative">
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2.5 font-mono text-sm font-bold text-slate-900 focus:ring-2 focus:ring-blue-500"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 font-bold">TL</span>
</div>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1.5">KDV Oranı</label>
<div className="flex gap-1.5">
{[1, 10, 20].map(rate => (
<button
key={rate}
type="button"
onClick={() => setVatRate(rate)}
className={`flex-1 py-2 rounded-lg font-extrabold transition-colors border ${
vatRate === rate ? 'bg-blue-600 text-white border-blue-600' : 'bg-slate-50 text-slate-700 border-slate-300 hover:bg-slate-100'
}`}
>
%{rate}
</button>
))}
</div>
</div>
<div>
<label className="block font-bold text-slate-700 mb-1.5">Tevkifat Oranı (Varsa)</label>
<select
value={tevkifatRate}
onChange={(e) => setTevkifatRate(Number(e.target.value))}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2.5 font-semibold text-slate-800"
>
<option value={0}>Tevkifat Yok</option>
<option value={50}>5/10 Tevkifat (%50)</option>
<option value={70}>7/10 Tevkifat (%70)</option>
<option value={90}>9/10 Tevkifat (%90)</option>
</select>
</div>
</div>
{/* Results Card */}
<div className="bg-slate-900 text-white p-6 rounded-xl space-y-4 shadow-lg">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<span className="font-bold text-xs uppercase tracking-wider text-slate-300">Hesaplama Sonucu</span>
<button
onClick={handleCopy}
className="bg-slate-800 hover:bg-slate-700 text-blue-400 font-semibold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 transition-colors border border-slate-700"
>
{copied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
<span>{copied ? 'Kopyalandı!' : 'Kopyala'}</span>
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs">
<div className="bg-white/5 p-3 rounded-lg border border-white/10">
<span className="text-slate-400 font-medium block">Net Matrah (KDV Hariç)</span>
<span className="text-lg font-black text-white mt-1 block">
₺{netAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
</span>
</div>
<div className="bg-white/5 p-3 rounded-lg border border-white/10">
<span className="text-slate-400 font-medium block">Hesaplanan KDV (%{vatRate})</span>
<span className="text-lg font-black text-amber-400 mt-1 block">
₺{vatAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
</span>
</div>
<div className="bg-white/5 p-3 rounded-lg border border-white/10">
<span className="text-slate-400 font-medium block">Genel Toplam (KDV Dahil)</span>
<span className="text-xl font-black text-emerald-400 mt-1 block">
₺{grossAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}
</span>
</div>
</div>
{tevkifatRate > 0 && (
<div className="bg-blue-950/80 p-3.5 rounded-lg border border-blue-800 text-xs flex justify-between">
<span className="text-slate-300">Tevkifat Tutarı: <strong className="text-amber-300">₺{tevkifatAmount.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}</strong></span>
<span className="text-slate-300">Ödenecek KDV: <strong className="text-emerald-400">₺{payableVat.toLocaleString('tr-TR', { maximumFractionDigits: 2 })}</strong></span>
</div>
)}
</div>
</div>
</div>
);
};
+297
View File
@@ -0,0 +1,297 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { MOCK_TENDERS, MOCK_LIVE_STATS, MOCK_COMPANY_STATS } from '../data/mockTenders';
const AppContext = createContext();
export const AppProvider = ({ children }) => {
const [activeTab, setActiveTab] = useState('list'); // 'list', 'bulten', 'sonuc', 'takvim', 'sozlesme', 'kdv', 'fiyat', 'kanban', 'smart_filter_builder'
const [tenders, setTenders] = useState(() => {
const saved = localStorage.getItem('ihaleist_tenders');
return saved ? JSON.parse(saved) : MOCK_TENDERS;
});
const [liveStats, setLiveStats] = useState(MOCK_LIVE_STATS);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCity, setSelectedCity] = useState('');
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
const [selectedSektor, setSelectedSektor] = useState('');
const [onlyValidOnly, setOnlyValidOnly] = useState(true);
const [quickFilter, setQuickFilter] = useState('all');
const [savedFilters, setSavedFilters] = useState(() => {
const saved = localStorage.getItem('ihaleist_saved_filters');
return saved ? JSON.parse(saved) : [
{ id: 'sf-1', name: 'Sağlık Hizmeti İhaleleri', user: 'hikmet', kurumTipi: 'Eğitim ve Araştırma Hastaneleri', sehir: 'Ankara' },
{ id: 'sf-2', name: 'Kayseri Kırtasiye Alımları', user: 'hikmet', kurumTipi: 'Ceza İnfaz Kurumları', sehir: 'Kayseri' }
];
});
const [activeTenderDetail, setActiveTenderDetail] = useState(null);
const [isDetailedSearchOpen, setIsDetailedSearchOpen] = useState(false);
const [isCompanyScannerOpen, setIsCompanyScannerOpen] = useState(false);
const [isAiSpecAnalyzerOpen, setIsAiSpecAnalyzerOpen] = useState(false);
const [aiAnalysisResults, setAiAnalysisResults] = useState(null);
// Enterprise Module States
const [competitorWatchlist, setCompetitorWatchlist] = useState([
{ id: 'cw-1', name: 'Bileşim Tıbbi Cihazlar A.Ş.', sector: 'Medikal & Tıbbi Malzeme', winCount: 24, totalRevenue: 14200000, marketShare: 32, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
{ id: 'cw-2', name: 'Atlas Sağlık ve Kırtasiye Ltd.', sector: 'Sağlık & Büro Malzemeleri', winCount: 18, totalRevenue: 9800000, marketShare: 24, status: 'Aktif İzleniyor', alertOnBid: true, alertOnWin: true },
{ id: 'cw-3', name: 'Tekno-Med Laboratuvar Sistemleri', sector: 'Laboratuvar & Bilişim', winCount: 12, totalRevenue: 6500000, marketShare: 16, status: 'Aktif İzleniyor', alertOnBid: false, alertOnWin: true }
]);
const [vaultDocuments, setVaultDocuments] = useState([
{ id: 'doc-1', name: 'İmza Sirküleri 2026.pdf', category: 'Yasal Belgeler', size: '2.4 MB', updatedAt: '2026-01-15', expiresAt: '2027-01-15', status: 'Geçerli' },
{ id: 'doc-2', name: 'ISO 13485 Tıbbi Cihaz Sertifikası.pdf', category: 'Kalite Belgeleri', size: '1.8 MB', updatedAt: '2025-11-10', expiresAt: '2028-11-10', status: 'Geçerli' },
{ id: 'doc-3', name: 'Oda Faaliyet Belgesi.pdf', category: 'Resmi Kayıtlar', size: '1.1 MB', updatedAt: '2026-02-01', expiresAt: '2026-08-01', status: 'Geçerli' },
{ id: 'doc-4', name: 'ÜTS Firma Kayıt Belgesi.pdf', category: 'Tıbbi Cihaz Kaydı', size: '3.2 MB', updatedAt: '2026-03-20', expiresAt: '2027-03-20', status: 'Geçerli' },
{ id: 'doc-5', name: 'Vergi Levhası 2025.pdf', category: 'Mali Belgeler', size: '850 KB', updatedAt: '2025-05-30', expiresAt: '2026-05-30', status: 'Geçerli' }
]);
const [reminderSettings, setReminderSettings] = useState({
emailEnabled: true,
smsEnabled: true,
pushEnabled: true,
notify3DaysBefore: true,
notify1DayBefore: true,
notify3HoursBefore: true,
googleCalendarSync: true,
outlookSync: false
});
const [webhooksConfig, setWebhooksConfig] = useState({
slackWebhookUrl: 'https://hooks.slack.com/services/T00/B00/XXXXXX',
teamsWebhookUrl: 'https://outlook.office.com/webhook/XXXXXX',
events: {
newTenderMatch: true,
tenderWon: true,
bidDeadlineAlert: true,
competitorActivity: true
}
});
const [apiKeys, setApiKeys] = useState([
{ id: 'key-1', name: 'ERP Entegrasyon Anahtarı', key: 'ih_live_8f92a19c3b88412e', createdAt: '2026-04-10', lastUsed: 'Bugün 12:45' }
]);
const analyzeSpecPdfWithGemini = (fileName = 'Teknik_Sartname.pdf') => {
return new Promise((resolve) => {
setTimeout(() => {
const result = {
fileName,
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 border', 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ı'
}
};
setAiAnalysisResults(result);
resolve(result);
}, 2200);
});
};
const [userProfile, setUserProfile] = useState({
name: 'Hikmet Tanrıverdi',
username: 'tanriverdi',
company: 'Tanrıverdi Medikal & Teknolojileri A.Ş.',
website: 'https://www.tanriverdimedikal.com.tr',
trialDaysLeft: 7,
role: 'Yönetici Tedarikçi',
scrapedCapabilities: {
scrapedAt: '2026-08-07 14:30',
extractedSectors: ['Sağlık / Tıbbi Malzeme', 'Kırtasiye & Büro', 'Temizlik & Hijyen'],
extractedProducts: ['Ortopedi Protez Setleri', 'Cerrahi Maske', 'Dezenfektan', 'Hasta Yatağı', 'Sanatsal Kırtasiye'],
extractedCertifications: ['ISO 13485 Tıbbi Cihaz Kalite', 'ISO 9001', 'ÜTS Ürün Kayıt Yetkisi', 'CE Uygunluk'],
matchScoreBoost: 15
}
});
const scanCompanyWebsite = (url) => {
return new Promise((resolve) => {
setTimeout(() => {
const domain = url.replace(/https?:\/\//, '').replace(/\/.*$/, '');
const newCapabilities = {
scrapedAt: new Date().toLocaleString('tr-TR'),
extractedSectors: ['Sağlık / Tıbbi Malzeme', 'Bilişim & Yazılım', 'Kırtasiye & Büro'],
extractedProducts: [
`${domain} - Medikal Cihaz ve Sarfları`,
'Laboratuvar Test Kiti',
'Zayıf Akım Güvenlik Sistemleri',
'Ofis & Büro Kırtasiye Malzemeleri'
],
extractedCertifications: [
'ISO 13485 Medikal Kalite',
'ISO 27001 Bilgi Güvenliği',
'ÜTS Ürün Takip Sistemi Belgesi',
'TSE Hizmet Yeterlilik Belgesi'
],
matchScoreBoost: 20
};
setUserProfile(prev => ({
...prev,
website: url,
scrapedCapabilities: newCapabilities
}));
resolve(newCapabilities);
}, 2500);
});
};
const [notifications, setNotifications] = useState([
{ id: 'n1', title: 'Yeni İhale Bildirimi', text: 'Sağlık Hizmeti İhaleleri filtrenize 3 yeni ihale eklendi.', time: '5dk önce', read: false },
{ id: 'n2', title: 'Yaklaşan İhale Süresi', text: 'KIRTASİYE MALZEMELERİ ALIMI ihaleniz için son 3 gün!', time: '1sa önce', read: false }
]);
// Persist state to localstorage
useEffect(() => {
localStorage.setItem('ihaleist_tenders', JSON.stringify(tenders));
}, [tenders]);
useEffect(() => {
localStorage.setItem('ihaleist_saved_filters', JSON.stringify(savedFilters));
}, [savedFilters]);
// Live counter animation effect
useEffect(() => {
const interval = setInterval(() => {
setLiveStats(prev => ({
...prev,
onlineKullanici: prev.onlineKullanici + (Math.floor(Math.random() * 5) - 2),
bugunEklenen: prev.bugunEklenen + (Math.random() > 0.7 ? 1 : 0)
}));
}, 4000);
return () => clearInterval(interval);
}, []);
const toggleTrackTender = (tenderId) => {
setTenders(prev => prev.map(t => {
if (t.id === tenderId) {
return { ...t, takipte: !t.takipte };
}
return t;
}));
};
const updateKanbanStatus = (tenderId, newStatus, assignedUser = null) => {
setTenders(prev => prev.map(t => {
if (t.id === tenderId) {
return {
...t,
kanbanStatus: newStatus,
atananPersonel: assignedUser !== null ? assignedUser : t.atananPersonel,
takipte: true
};
}
return t;
}));
};
const addSavedFilter = (newFilter) => {
const filterObj = {
id: 'sf-' + Date.now(),
user: userProfile.username,
...newFilter
};
setSavedFilters(prev => [filterObj, ...prev]);
};
const deleteSavedFilter = (id) => {
setSavedFilters(prev => prev.filter(f => f.id !== id));
};
const clearAllFilters = () => {
setSearchQuery('');
setSelectedCity('');
setSelectedAlimTuru('');
setSelectedKurumTipi('');
setSelectedSektor('');
setQuickFilter('all');
};
return (
<AppContext.Provider value={{
activeTab,
setActiveTab,
tenders,
setTenders,
liveStats,
searchQuery,
setSearchQuery,
selectedCity,
setSelectedCity,
selectedAlimTuru,
setSelectedAlimTuru,
selectedKurumTipi,
setSelectedKurumTipi,
selectedSektor,
setSelectedSektor,
onlyValidOnly,
setOnlyValidOnly,
quickFilter,
setQuickFilter,
savedFilters,
addSavedFilter,
deleteSavedFilter,
activeTenderDetail,
setActiveTenderDetail,
isDetailedSearchOpen,
setIsDetailedSearchOpen,
isCompanyScannerOpen,
setIsCompanyScannerOpen,
scanCompanyWebsite,
isAiSpecAnalyzerOpen,
setIsAiSpecAnalyzerOpen,
analyzeSpecPdfWithGemini,
aiAnalysisResults,
competitorWatchlist,
setCompetitorWatchlist,
vaultDocuments,
setVaultDocuments,
reminderSettings,
setReminderSettings,
webhooksConfig,
setWebhooksConfig,
apiKeys,
setApiKeys,
userProfile,
notifications,
toggleTrackTender,
updateKanbanStatus,
clearAllFilters
}}>
{children}
</AppContext.Provider>
);
};
export const useApp = () => useContext(AppContext);
+331
View File
@@ -0,0 +1,331 @@
export const MOCK_TENDERS = [
{
id: "tender-101",
ikn: "26DT1489671",
baslik: "KIRTASİYE MALZEMELERİ ALIMI KAYSERİ ÇOCUK CEZAEVİ",
kurum: "CEZA VE TEVKİFEVLERİ GENEL MÜDÜRLÜĞÜ KAYSERİ ÇOCUK KAPALI CEZA İNFAZ KURUMU MÜDÜRLÜĞÜ",
sehir: "Kayseri",
bolge: "İÇ ANADOLU BÖLGESİ",
tarih: "10.08.2026",
saat: "12:00",
alimTuru: "Doğrudan Temin",
usul: "Doğrudan Temin (4734 22/d)",
sektor: "Kırtasiye & Büro",
brans: "Sanatsal Kırtasiye ve El Becerisi Malzemeleri",
kurumTipi: "Ceza İnfaz Kurumları",
eklenmeZamani: "az önce eklendi",
isYeni: true,
aciliyetBadge: "⚠️ SON 3 GÜN",
items: [
{ id: "i1", ad: "35*50 TUVAL", miktar: "10", birim: "adet", birimFiyat: 145 },
{ id: "i2", ad: "50*70 TUVAL", miktar: "10", birim: "adet", birimFiyat: 220 },
{ id: "i3", ad: "AKRİLİK BOYA SETİ 12'Lİ", miktar: "25", birim: "set", birimFiyat: 185 },
{ id: "i4", ad: "RESİM FIRÇASI SETİ NO: 1-12", miktar: "30", birim: "set", birimFiyat: 95 }
],
aciklama: "Kayseri Çocuk Kapalı Ceza İnfaz Kurumu Müdürlüğü Hobi ve Atölye Çalışmaları İhtiyacı İçin 4 Kalem Kırtasiye ve Sanatsal Malzeme Doğrudan Temin Usulü İle Alınacaktır. Fiyat teklif cetveli kaşeli ve imzalı teslim edilecektir.",
yaklasikMaliyet: 18500,
teslimatSuresi: "7 Takvim Günü",
teslimYeri: "Kayseri Çocuk Cezaevi Deposu",
yeterlikKriterleri: ["KDV Hariç Teklif Cetveli", "İmza Sirküleri / Yetki Belgesi", "Faaliyet Belgesi"],
aiMatchScore: 94,
aiRiskScore: "Düşük",
aiBidRange: { min: 14200, max: 17800, recommended: 15600 },
kanbanStatus: "teklif_hazirlaniyor",
atananPersonel: "Ahmet Yılmaz",
durum: "Aktif",
incelendi: true,
takipte: true,
sozlesmeBedeli: null,
kazananFirma: null
},
{
id: "tender-102",
ikn: "26DT1489955",
baslik: "Yemek Kabı ve Üst Kapama Filmi Alımı",
kurum: "TARIM İŞLETMELERİ GENEL MÜDÜRLÜĞÜ KARACABEY TARIM İŞLETMESİ MÜDÜRLÜĞÜ",
sehir: "Bursa",
bolge: "MARMARA BÖLGESİ",
tarih: "10.08.2026",
saat: "18:00",
alimTuru: "Doğrudan Temin",
usul: "Doğrudan Temin (4734 22/d)",
sektor: "Gıda & Catering",
brans: "Ambalaj ve Paketleme Ürünleri",
kurumTipi: "Kamu Şirketleri",
eklenmeZamani: "az önce eklendi",
isYeni: true,
aciliyetBadge: "⚠️ SON 3 GÜN",
items: [
{ id: "i1", ad: "1 Gözlü Yemek Kabı", miktar: "1000", birim: "adet", birimFiyat: 2.80 },
{ id: "i2", ad: "2 Gözlü Yemek Kabı", miktar: "1000", birim: "adet", birimFiyat: 3.40 },
{ id: "i3", ad: "Sızdırmaz Üst Kapama Filmi (Rulo)", miktar: "50", birim: "rulo", birimFiyat: 410 }
],
aciklama: "Karacabey Tarım İşletmesi personel yemekhanesi için kullanılacak olan ısıya dayanıklı 1 ve 2 gözlü porsiyonluk kaplar ve üst kapatma film ruloları alınacaktır.",
yaklasikMaliyet: 26700,
teslimatSuresi: "5 İş Günü",
teslimYeri: "Karacabey Tarım İşletmesi Ambarı",
yeterlikKriterleri: ["Gıda Temas Uygunluk Sertifikası (ISO 22000)", "Teklif Mektubu"],
aiMatchScore: 88,
aiRiskScore: "Düşük",
aiBidRange: { min: 21000, max: 25500, recommended: 23200 },
kanbanStatus: "incelenecek",
atananPersonel: null,
durum: "Aktif",
incelendi: false,
takipte: false,
sozlesmeBedeli: null,
kazananFirma: null
},
{
id: "tender-103",
ikn: "2026/184201",
baslik: "193064-PANİK BUTON KURULUM İŞİ",
kurum: "ANKARA ÜNİVERSİTESİ REKTÖRLÜĞÜ SAĞLIK UYGULAMA VE ARAŞTIRMA HASTANELERİ BAŞHEKİMLİĞİ",
sehir: "Ankara",
bolge: "İÇ ANADOLU BÖLGESİ",
tarih: "11.08.2026",
saat: "10:30",
alimTuru: "Doğrudan Temin",
usul: "Doğrudan Temin (4734 22/d)",
sektor: "Bilişim & Yazılım",
brans: "Güvenlik Sistemleri ve Zayıf Akım",
kurumTipi: "Üniversiteler",
eklenmeZamani: "15 dakika önce",
isYeni: true,
aciliyetBadge: "🔥 BUGÜN KAPANIYOR",
items: [
{ id: "i1", ad: "Kablosuz Panik Butonu Verici Ünite", miktar: "15", birim: "adet", birimFiyat: 1250 },
{ id: "i2", ad: "Merkezi Alarm Alma Paneli (RF Receiver)", miktar: "2", birim: "adet", birimFiyat: 8400 },
{ id: "i3", ad: "Montaj ve Kablolama Hizmeti", miktar: "1", birim: "paket", birimFiyat: 6500 }
],
aciklama: "İbn-i Sina Araştırma Hastanesi Acil Servis ve Yoğun Bakım üniteleri için acil durum kablosuz panik buton sistemi temini ve devreye alınması işidir.",
yaklasikMaliyet: 42000,
teslimatSuresi: "10 Gün",
teslimYeri: "Ankara Üniversitesi İbn-i Sina Hastanesi",
yeterlikKriterleri: ["TSE HYB Belgesi", "Marka Tescil / Yetkili Satıcı Belgesi"],
aiMatchScore: 92,
aiRiskScore: "Orta",
aiBidRange: { min: 34500, max: 40800, recommended: 37200 },
kanbanStatus: "inceleniyor",
atananPersonel: "Zeynep Kaya",
durum: "Aktif",
incelendi: true,
takipte: true,
sozlesmeBedeli: null,
kazananFirma: null
},
{
id: "tender-104",
ikn: "26DT1490112",
baslik: "HASTAYA ÖZGÜ ORTOPEDİ MALZEME ALIMI (ZARİFE BALLI)",
kurum: "KONYA İL SAĞLIK MÜDÜRLÜĞÜ KONYA AKŞEHİR DEVLET HASTANESİ BAŞHEKİMLİĞİ",
sehir: "Konya",
bolge: "İÇ ANADOLU BÖLGESİ",
tarih: "12.08.2026",
saat: "14:00",
alimTuru: "Doğrudan Temin",
usul: "Doğrudan Temin (4734 22/f)",
sektor: "Sağlık / Tıbbi Malzeme",
brans: "Ortopedi ve Protez İmplanta Sistemleri",
kurumTipi: "Eğitim ve Araştırma Hastaneleri",
eklenmeZamani: "1 saat önce",
isYeni: false,
aciliyetBadge: null,
items: [
{ id: "i1", ad: "Revizyon Kalça Protez Femoral Stem (Titanyum)", miktar: "1", birim: "set", birimFiyat: 48500 },
{ id: "i2", ad: "Asetabular Kap ve Seramik Insert", miktar: "1", birim: "set", birimFiyat: 29000 },
{ id: "i3", ad: "Kemik Çimentosu Gentamisinli 40gr", miktar: "2", birim: "kutu", birimFiyat: 3200 }
],
aciklama: "Hastaya özel doğrudan temin usulü ile kalça revizyon protez seti alımıdır. Ürünlerin ÜTS (Ürün Takip Sistemi) kaydı ve SUT kodları eksiksiz olmalıdır.",
yaklasikMaliyet: 83900,
teslimatSuresi: "3 Gün (Hasta Ameliyat Tarihi)",
teslimYeri: "Akşehir Devlet Hastanesi Ameliyathane Deposu",
yeterlikKriterleri: ["ÜTS Barkod Eşleşme Çıktısı", "SUT Bildirimi", "FDA / CE Sertifikası"],
aiMatchScore: 96,
aiRiskScore: "Düşük",
aiBidRange: { min: 72000, max: 81000, recommended: 76500 },
kanbanStatus: "teklif_verildi",
atananPersonel: "Hikmet Tanrıverdi",
durum: "Aktif",
incelendi: true,
takipte: true,
sozlesmeBedeli: null,
kazananFirma: null
},
{
id: "tender-105",
ikn: "2026/178920",
baslik: "Klima Santrali Filtre Değişimi ve Bakım Hizmet Alımı",
kurum: "İZMİR BÜYÜKŞEHİR BELEDİYESİ ULAŞIM DARE BAŞKANLIĞI",
sehir: "İzmir",
bolge: "EGE BÖLGESİ",
tarih: "14.08.2026",
saat: "11:00",
alimTuru: "Açık İhale",
usul: "Açık İhale Usulü (4734 Madde 19)",
sektor: "İnşaat & Bakım",
brans: "HVAC ve İklimlendirme Bakımı",
kurumTipi: "Büyükşehir Belediyeleri",
eklenmeZamani: "3 saat önce",
isYeni: false,
aciliyetBadge: null,
items: [
{ id: "i1", ad: "G4 Panel Filtre 592x592x48mm", miktar: "120", birim: "adet", birimFiyat: 190 },
{ id: "i2", ad: "F7 Torba Filtre 592x592x535mm 6 Cepli", miktar: "80", birim: "adet", birimFiyat: 480 },
{ id: "i3", ad: "Hepa Filtre H13 610x610x292mm", miktar: "24", birim: "adet", birimFiyat: 2100 },
{ id: "i4", ad: "Periyodik Bakım ve Gaz Şarjı Hizmeti", miktar: "12", birim: "ay", birimFiyat: 15000 }
],
aciklama: "İzmir Büyükşehir Belediyesi Hizmet Binalarındaki 18 Adet Klima Santralinin 1 Yıllık Periyodik Filtre Değişimi ve Bakım Hizmeti Alımı İhalesidir.",
yaklasikMaliyet: 241000,
teslimatSuresi: "365 Gün",
teslimYeri: "İzmir Merkez Binaları",
yeterlikKriterleri: ["İş Deneyim Belgesi (Benzer İş %50)", "Geçici Teminat Mektubu %3", "Kapasite Raporu"],
aiMatchScore: 81,
aiRiskScore: "Orta",
aiBidRange: { min: 195000, max: 228000, recommended: 212000 },
kanbanStatus: "incelenecek",
atananPersonel: null,
durum: "Aktif",
incelendi: false,
takipte: false,
sozlesmeBedeli: null,
kazananFirma: null
},
{
id: "tender-106",
ikn: "26DT1490888",
baslik: "Genel Temizlik ve Dezenfeksiyon Sarf Malzemeleri Alımı",
kurum: "ANTALYA GENÇLİK VE SPOR İL MÜDÜRLÜĞÜ KYK YURTLARI",
sehir: "Antalya",
bolge: "AKDENİZ BÖLGESİ",
tarih: "15.08.2026",
saat: "15:30",
alimTuru: "Doğrudan Temin",
usul: "Doğrudan Temin (4734 22/d)",
sektor: "Temizlik & Hijyen",
brans: "Endüstriyel Temizlik Kimyasalları",
kurumTipi: "Bakanlıklar",
eklenmeZamani: "5 saat önce",
isYeni: false,
aciliyetBadge: null,
items: [
{ id: "i1", ad: "Çamaşır Suyu (Yüzey Dezenfektanı) 20 L", miktar: "80", birim: "bidon", birimFiyat: 340 },
{ id: "i2", ad: "Sıvı El Sabunu Sedefli 30 L", miktar: "60", birim: "bidon", birimFiyat: 520 },
{ id: "i3", ad: "Mikrofiber Temizlik Bezi 40x40 (100'lü)", miktar: "15", birim: "paket", birimFiyat: 680 }
],
aciklama: "Antalya KYK Kız ve Erkek Öğrenci Yurtları yeni eğitim-öğretim sezonu hazırlıkları için temizlik hijyen sarf ürünleri temin edilecektir.",
yaklasikMaliyet: 68600,
teslimatSuresi: "7 Gün",
teslimYeri: "Antalya Akdeniz Yurt Müdürlüğü Deposu",
yeterlikKriterleri: ["MSDS Güvenlik Bilgi Formları", "Sağlık Bakanlığı Ruhsat Fotokopisi"],
aiMatchScore: 89,
aiRiskScore: "Düşük",
aiBidRange: { min: 54000, max: 63000, recommended: 58500 },
kanbanStatus: "kazanildi",
atananPersonel: "Hikmet Tanrıverdi",
durum: "Sonuçlandı",
incelendi: true,
takipte: true,
sozlesmeBedeli: 57400,
kazananFirma: "Tanrıverdi Medikal & Hijyen Ltd. Şti."
},
{
id: "tender-107",
ikn: "2026/172340",
baslik: "Veri Merkezi Sunucu ve Depolama Ünitesi Alımı",
kurum: "HAVELSAN HAVA ELEKTRONİK SANAYİ A.Ş.",
sehir: "Ankara",
bolge: "İÇ ANADOLU BÖLGESİ",
tarih: "18.08.2026",
saat: "10:00",
alimTuru: "Açık İhale",
usul: "Açık İhale",
sektor: "Bilişim & Yazılım",
brans: "Sunucu ve Veri Depolama Donanımları",
kurumTipi: "Kamu Şirketleri",
eklenmeZamani: "1 gün önce",
isYeni: false,
aciliyetBadge: null,
items: [
{ id: "i1", ad: "Rack Sunucu 2U 2x Xeon Gold 6430 / 256GB RAM", miktar: "4", birim: "adet", birimFiyat: 145000 },
{ id: "i2", ad: "NVMe SAN Storage Unit 100TB Usable", miktar: "1", birim: "set", birimFiyat: 620000 }
],
aciklama: "Havelsan Teknoloji Kampüsü Veri Merkezi Genişletme Projesi kapsamında yüksek performanslı sunucu ve veri depolama üniteleri temin edilecektir.",
yaklasikMaliyet: 1200000,
teslimatSuresi: "30 Takvim Günü",
teslimYeri: "Havelsan ODTÜ Teknokent Binası",
yeterlikKriterleri: ["Üretici Yetki Belgesi (MAF)", "ISO 27001 Bilgi Güvenliği Belgesi"],
aiMatchScore: 76,
aiRiskScore: "Yüksek",
aiBidRange: { min: 980000, max: 1150000, recommended: 1040000 },
kanbanStatus: "kaybedildi",
atananPersonel: "Mehmet Demir",
durum: "Sonuçlandı",
incelendi: true,
takipte: true,
sozlesmeBedeli: 1025000,
kazananFirma: "Netaş Telekomünikasyon A.Ş."
}
];
export const MOCK_COMPANY_STATS = {
notlarimCount: 4,
ajandaCount: 2,
incelediklerimCount: 14,
sozlesmelerimCount: 8,
sonuclarimCount: 12,
takibimdekilerCount: 9,
kanbanStats: {
incelenecek: 3,
inceleniyor: 2,
teklif_hazirlaniyor: 4,
teklif_verildi: 3,
kazanildi: 8,
kaybedildi: 4
}
};
export const MOCK_LIVE_STATS = {
onlineKullanici: 17551,
toplamKalem: 99666,
bugunEklenen: 1445,
aktifTakip: 102473
};
export const SECTORS_LIST = [
"Sağlık / Tıbbi Malzeme",
"Kırtasiye & Büro",
"Bilişim & Yazılım",
"Gıda & Catering",
"İnşaat & Bakım",
"Temizlik & Hijyen",
"Tekstil & Giyim",
"Otomotiv & Yedek Parça"
];
export const INSTITUTION_TYPES_LIST = [
"Eğitim ve Araştırma Hastaneleri",
"Belediyeler",
"Büyükşehir Belediyeleri",
"Ceza İnfaz Kurumları",
"Üniversiteler",
"Bakanlıklar",
"Kamu Şirketleri",
"Ağız ve Diş Sağlığı Merkezleri"
];
export const CITIES_LIST = [
"Adana", "Adıyaman", "Afyonkarahisar", "Aksaray", "Amasya", "Ankara", "Antalya", "Ardahan", "Artvin",
"Aydın", "Balıkesir", "Bolu", "Bursa", "Çanakkale", "Denizli", "Diyarbakır", "Edirne", "Eskişehir",
"Gaziantep", "Giresun", "Isparta", "İstanbul", "İzmir", "Kahramanmaraş", "Kars", "Kastamonu", "Kayseri",
"Kocaeli", "Konya", "Kütahya", "Malatya", "Manisa", "Mardin", "Muğla", "Ordu", "Rize", "Sakarya",
"Samsun", "Sivas", "Şanlıurfa", "Tekirdağ", "Trabzon", "Van", "Zonguldak"
];
export const COMPETITOR_DATA = [
{ name: "Tanrıverdi Medikal & Hijyen Ltd.", winCount: 24, totalVolume: "4.850.000 TL", share: 32 },
{ name: "Atlas Büro & Kırtasiye A.Ş.", winCount: 19, totalVolume: "3.200.000 TL", share: 24 },
{ name: "ProTek Bilişim ve Sistemler", winCount: 15, totalVolume: "6.750.000 TL", share: 22 },
{ name: "Özkan Sağlık Hizmetleri", winCount: 11, totalVolume: "2.100.000 TL", share: 14 },
{ name: "Diğer Tedarikçiler", winCount: 8, totalVolume: "1.400.000 TL", share: 8 }
];
+45
View File
@@ -0,0 +1,45 @@
@import "tailwindcss";
@layer base {
body {
background-color: #ebedf2;
color: #1e293b;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out forwards;
}
/* Pulse badge animation */
@keyframes pulseGlow {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.85; transform: scale(1.03); }
}
.pulse-glow {
animation: pulseGlow 2s infinite ease-in-out;
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
port: 3000,
host: true
}
})