feat: İhaleist - Akıllı İhale Takip ve Analiz Platformu (Full PRD + 7 Enterprise Modül)
This commit is contained in:
+81
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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 }
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
Reference in New Issue
Block a user