Files
ihaleist/src/components/LeftSidebar.jsx
T

415 lines
19 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};