415 lines
19 KiB
React
415 lines
19 KiB
React
import React, {useState, useEffect } from 'react';
|
||
import { useApp } from '../context/AppContext';
|
||
import { INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/constants';
|
||
import {
|
||
Compass, Zap, Wrench, Calendar, Eye, EyeOff, FileText, CheckCircle2, XCircle, Archive,
|
||
Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag, Star, Hash
|
||
} from 'lucide-react';
|
||
|
||
export const LeftSidebar = ({onOpenProductSearch} = {}) => {
|
||
const {
|
||
favoriteKurumlar,
|
||
quickFilter,
|
||
setQuickFilter,
|
||
setActiveTab,
|
||
savedFilters,
|
||
deleteSavedFilter,
|
||
selectedAlimTuru,
|
||
setSelectedAlimTuru,
|
||
selectedSektorler,
|
||
setSelectedSektorler,
|
||
toggleSektor,
|
||
sektorListesi,
|
||
selectedCpvler,
|
||
setSelectedCpvler,
|
||
toggleCpv,
|
||
cpvBolumListesi,
|
||
setIsUrunAramaOpen,
|
||
setUrunAramaBaslangic,
|
||
selectedKurumTipi,
|
||
setSelectedKurumTipi,
|
||
selectedCity,
|
||
setSelectedCity,
|
||
clearAllFilters
|
||
} = useApp();
|
||
|
||
const [sektorAra, setSektorAra] = useState('');
|
||
const [cpvAra, setCpvAra] = useState('');
|
||
// OKAS kataloğu: idarenin kendi kodu. "ortopedi" başlıkta geçmese de kod yakalar.
|
||
const [okasQ, setOkasQ] = useState('');
|
||
const [okasSonuc, setOkasSonuc] = useState([]);
|
||
useEffect(() => {
|
||
const t = okasQ.trim();
|
||
if (t.length < 2) { setOkasSonuc([]); return; }
|
||
let iptal = false;
|
||
const z = setTimeout(() => import('../services/kalem')
|
||
.then(({ okasAra }) => okasAra(t))
|
||
.then((k) => { if (!iptal) setOkasSonuc(k.slice(0, 12)); })
|
||
.catch(() => {}), 350);
|
||
return () => { iptal = true; clearTimeout(z); };
|
||
}, [okasQ]);
|
||
const okasIleAra = (k) => {
|
||
onOpenProductSearch?.();
|
||
setUrunAramaBaslangic({ okasKod: k.kod, okasAd: k.ad });
|
||
setIsUrunAramaOpen(true);
|
||
};
|
||
const [openSections, setOpenSections] = useState({
|
||
alimTuru: true,
|
||
sektorler: true,
|
||
cpv: false,
|
||
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: 'favori_kurumlar', label: 'Favori Kurumlarım', icon: Star, badge: favoriteKurumlar?.length ? String(favoriteKurumlar.length) : null },
|
||
{ 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' || selectedSektorler.length || selectedCpvler.length || 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">
|
||
{/* 25 sektör var; aramadan bulmak zor */}
|
||
<input
|
||
value={sektorAra}
|
||
onChange={(e) => setSektorAra(e.target.value)}
|
||
placeholder="Sektör ara…"
|
||
className="w-full border border-slate-200 rounded-lg px-2 py-1.5 mb-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-blue-500/30"
|
||
/>
|
||
<div className="max-h-56 overflow-y-auto space-y-1">
|
||
<button
|
||
onClick={() => { setSelectedSektorler([]); setActiveTab('list'); }}
|
||
className={`w-full text-left px-2 py-1 rounded font-medium ${!selectedSektorler.length ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
|
||
>
|
||
Tüm Sektörler {selectedSektorler.length > 0 && <span className="text-slate-400">({selectedSektorler.length} seçili)</span>}
|
||
</button>
|
||
{/* Çoklu seçim: kutucuklar birikir, liste kapanmaz */}
|
||
{sektorListesi
|
||
.filter((sec) => sec.toLocaleLowerCase('tr').includes(sektorAra.toLocaleLowerCase('tr')))
|
||
.map(sec => {
|
||
const secili = selectedSektorler.includes(sec);
|
||
return (
|
||
<label
|
||
key={sec}
|
||
className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer font-medium ${secili ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={secili}
|
||
onChange={() => { toggleSektor(sec); setActiveTab('list'); }}
|
||
className="accent-blue-600 shrink-0"
|
||
/>
|
||
<span className="truncate">{sec}</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* CPV Kodu */}
|
||
<div>
|
||
<button
|
||
onClick={() => toggleSection('cpv')}
|
||
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">
|
||
<Hash className="w-4 h-4 text-blue-600" /> CPV Kodu
|
||
</span>
|
||
{openSections.cpv ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
|
||
</button>
|
||
{openSections.cpv && (
|
||
<div className="p-3 space-y-1 text-xs">
|
||
<p className="text-[11px] text-slate-500 mb-2 leading-relaxed">
|
||
CPV, AB'nin ihale konusunu sınıflandırdığı standart koddur. Türkiye'deki NACE/faaliyet kodunun yerine geçmez — NACE firmanızı, CPV ihalenin konusunu sınıflandırır.
|
||
</p>
|
||
{/* OKAS: EKAP'ta idarenin seçtiği kod. Bulunan kodla EKAP'ta arama açılır. */}
|
||
<div className="mb-2 relative">
|
||
<input
|
||
value={okasQ}
|
||
onChange={(e) => setOkasQ(e.target.value)}
|
||
placeholder="OKAS kodu bul: ortopedik, röntgen…"
|
||
className="w-full border border-indigo-200 bg-indigo-50/40 rounded-lg px-2 py-1.5 text-xs focus:outline-hidden focus:ring-2 focus:ring-indigo-500/30"
|
||
/>
|
||
{okasSonuc.length > 0 && (
|
||
<div className="absolute z-20 left-0 right-0 mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-52 overflow-y-auto">
|
||
{okasSonuc.map((k) => (
|
||
<button key={k.kod} type="button" onClick={() => { okasIleAra(k); setOkasQ(''); }}
|
||
title="Bu OKAS koduyla EKAP'ta ara"
|
||
className="w-full text-left px-2 py-1.5 text-[11px] hover:bg-indigo-50 flex items-baseline gap-1.5">
|
||
<span className="font-mono font-bold text-indigo-700 shrink-0">{k.kod}</span>
|
||
<span className="text-slate-700 truncate">{k.ad}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<input
|
||
value={cpvAra}
|
||
onChange={(e) => setCpvAra(e.target.value)}
|
||
placeholder="CPV ara…"
|
||
className="w-full border border-slate-200 rounded-lg px-2 py-1.5 mb-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-blue-500/30"
|
||
/>
|
||
<div className="max-h-56 overflow-y-auto space-y-1">
|
||
<button
|
||
onClick={() => { setSelectedCpvler([]); setActiveTab('list'); }}
|
||
className={`w-full text-left px-2 py-1 rounded font-medium ${!selectedCpvler.length ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
|
||
>
|
||
Tüm CPV Kodları {selectedCpvler.length > 0 && <span className="text-slate-400">({selectedCpvler.length} seçili)</span>}
|
||
</button>
|
||
{/* Çoklu seçim: kutucuklar birikir, liste kapanmaz */}
|
||
{cpvBolumListesi
|
||
.filter((c) => c.kod.includes(cpvAra) || c.etiket.toLocaleLowerCase('tr').includes(cpvAra.toLocaleLowerCase('tr')))
|
||
.map(c => {
|
||
const secili = selectedCpvler.includes(c.kod);
|
||
return (
|
||
<label
|
||
key={c.kod}
|
||
className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer font-medium ${secili ? 'bg-blue-50 text-blue-700 font-bold' : 'text-slate-600 hover:bg-slate-50'}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={secili}
|
||
onChange={() => { toggleCpv(c.kod); setActiveTab('list'); }}
|
||
className="accent-blue-600 shrink-0"
|
||
/>
|
||
<span className="truncate">{c.kod} — {c.etiket}</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
</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>
|
||
);
|
||
};
|