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