feat: 30 günlük havuz referansı (kalıcı erime) + 25 sektör, çoklu sektör seçimi ve sunucudan gelen tek kaynaklı liste
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { SECTORS_LIST, CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||
import { CITIES_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||
import { SlidersHorizontal, Search, RotateCcw, X } from 'lucide-react';
|
||||
|
||||
export const DetailedSearchModal = () => {
|
||||
@@ -13,8 +13,10 @@ export const DetailedSearchModal = () => {
|
||||
setSelectedCity,
|
||||
selectedAlimTuru,
|
||||
setSelectedAlimTuru,
|
||||
selectedSektor,
|
||||
setSelectedSektor,
|
||||
selectedSektorler,
|
||||
setSelectedSektorler,
|
||||
toggleSektor,
|
||||
sektorListesi,
|
||||
selectedKurumTipi,
|
||||
setSelectedKurumTipi,
|
||||
onlyValidOnly,
|
||||
@@ -40,7 +42,7 @@ export const DetailedSearchModal = () => {
|
||||
setSearchQuery('');
|
||||
setSelectedCity('');
|
||||
setSelectedAlimTuru('');
|
||||
setSelectedSektor('');
|
||||
setSelectedSektorler([]);
|
||||
setSelectedKurumTipi('');
|
||||
setIknQuery('');
|
||||
setKurumQuery('');
|
||||
@@ -116,16 +118,32 @@ export const DetailedSearchModal = () => {
|
||||
/>
|
||||
</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 className="md:col-span-2">
|
||||
<label className="block font-bold text-slate-700 mb-1">
|
||||
Sektör / Kategori
|
||||
<span className="font-normal text-slate-400 ml-1">
|
||||
(birden fazla seçebilirsiniz{selectedSektorler.length > 0 ? ` — ${selectedSektorler.length} seçili` : ''})
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto bg-slate-50 border border-slate-300 rounded-lg p-2">
|
||||
{sektorListesi.map((s) => {
|
||||
const secili = selectedSektorler.includes(s);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={s}
|
||||
onClick={() => toggleSektor(s)}
|
||||
className={`px-2.5 py-1 rounded-full border text-[11px] font-semibold transition-colors ${
|
||||
secili
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'bg-white border-slate-300 text-slate-600 hover:border-blue-400 hover:text-blue-700'
|
||||
}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/constants';
|
||||
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
|
||||
@@ -16,8 +16,10 @@ export const LeftSidebar = () => {
|
||||
deleteSavedFilter,
|
||||
selectedAlimTuru,
|
||||
setSelectedAlimTuru,
|
||||
selectedSektor,
|
||||
setSelectedSektor,
|
||||
selectedSektorler,
|
||||
setSelectedSektorler,
|
||||
toggleSektor,
|
||||
sektorListesi,
|
||||
selectedKurumTipi,
|
||||
setSelectedKurumTipi,
|
||||
selectedCity,
|
||||
@@ -25,6 +27,7 @@ export const LeftSidebar = () => {
|
||||
clearAllFilters
|
||||
} = useApp();
|
||||
|
||||
const [sektorAra, setSektorAra] = useState('');
|
||||
const [openSections, setOpenSections] = useState({
|
||||
alimTuru: true,
|
||||
sektorler: true,
|
||||
@@ -59,7 +62,7 @@ export const LeftSidebar = () => {
|
||||
<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) && (
|
||||
{(quickFilter !== 'all' || selectedSektorler.length || selectedCity || selectedAlimTuru || selectedKurumTipi) && (
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="text-[11px] font-medium text-blue-600 hover:underline"
|
||||
@@ -199,22 +202,42 @@ export const LeftSidebar = () => {
|
||||
{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">
|
||||
<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={() => { 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'}`}
|
||||
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
|
||||
Tüm Sektörler {selectedSektorler.length > 0 && <span className="text-slate-400">({selectedSektorler.length} seçili)</span>}
|
||||
</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>
|
||||
))}
|
||||
{/* Ç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>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { INSTITUTION_TYPES_LIST, CITIES_LIST, SECTORS_LIST } from '../data/constants';
|
||||
import { INSTITUTION_TYPES_LIST, CITIES_LIST } from '../data/constants';
|
||||
import { Bot, ArrowLeft, CheckCircle2, HelpCircle, Building2, MapPin, Tag } from 'lucide-react';
|
||||
|
||||
export const SmartFilterBuilder = () => {
|
||||
const { savedFilters, addSavedFilter, deleteSavedFilter, setActiveTab } = useApp();
|
||||
const { savedFilters, addSavedFilter, deleteSavedFilter, setActiveTab, sektorListesi } = useApp();
|
||||
|
||||
const [filterName, setFilterName] = useState('');
|
||||
const [selectedKurumTip, setSelectedKurumTip] = useState('');
|
||||
@@ -238,7 +238,7 @@ export const SmartFilterBuilder = () => {
|
||||
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 => (
|
||||
{sektorListesi.map(s => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const TenderList = () => {
|
||||
searchQuery,
|
||||
selectedCity,
|
||||
selectedAlimTuru,
|
||||
selectedSektor,
|
||||
selectedSektorler,
|
||||
selectedKurumTipi,
|
||||
quickFilter,
|
||||
onlyValidOnly,
|
||||
@@ -52,7 +52,7 @@ export const TenderList = () => {
|
||||
// Dropdown Filters
|
||||
if (selectedCity && normTr(tender.sehir) !== normTr(selectedCity)) return false;
|
||||
if (selectedAlimTuru && tender.alimTuru !== selectedAlimTuru) return false;
|
||||
if (selectedSektor && tender.sektor !== selectedSektor) return false;
|
||||
if (selectedSektorler.length && !selectedSektorler.includes(tender.sektor)) return false;
|
||||
if (selectedKurumTipi && tender.kurumTipi !== selectedKurumTipi) return false;
|
||||
|
||||
return true;
|
||||
@@ -62,7 +62,7 @@ export const TenderList = () => {
|
||||
if (sortBy === 'distant') return b.tarih.localeCompare(a.tarih);
|
||||
return 0;
|
||||
});
|
||||
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektor, selectedKurumTipi, quickFilter, sortBy]);
|
||||
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedKurumTipi, quickFilter, sortBy]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredTenders.length / itemsPerPage));
|
||||
const paginatedTenders = filteredTenders.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { getToken } from '../../services/auth';
|
||||
import { SECTORS_LIST } from '../../data/constants';
|
||||
|
||||
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles,
|
||||
* isteğe bağlıdır (varsayılan: tüm Türkiye).
|
||||
*/
|
||||
export const NotifySubscriptions = () => {
|
||||
const { authUser, favoriteKurumlar, userProfile } = useApp();
|
||||
const { authUser, favoriteKurumlar, userProfile, sektorListesi } = useApp();
|
||||
|
||||
const [abonelikler, setAbonelikler] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -212,7 +212,7 @@ export const NotifySubscriptions = () => {
|
||||
|
||||
<p className="text-[10px] text-slate-400 pt-1">Hazır sektörler:</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{SECTORS_LIST.map((s) => (
|
||||
{sektorListesi.map((s) => (
|
||||
<button key={s} onClick={() => konuEkle(s)}
|
||||
className="text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 px-2 py-1 rounded-md">
|
||||
+ {s}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperServi
|
||||
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
|
||||
import { restoreSession, clearSession, getStoredUser } from '../services/auth';
|
||||
import { getAllUserData, setUserData, getPlan } from '../services/account';
|
||||
import { SECTORS_LIST, INSTITUTION_TYPES_LIST } from '../data/constants';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
@@ -51,7 +52,26 @@ export const AppProvider = ({ children }) => {
|
||||
const [selectedCity, setSelectedCity] = useState('');
|
||||
const [selectedAlimTuru, setSelectedAlimTuru] = useState('');
|
||||
const [selectedKurumTipi, setSelectedKurumTipi] = useState('');
|
||||
const [selectedSektor, setSelectedSektor] = useState('');
|
||||
// Çoklu sektör seçimi. Tek seçimlik eski API (selectedSektor) uyumluluk
|
||||
// için korunur: dizinin ilk elemanını okur, yazınca diziyi tek elemana indirir.
|
||||
const [selectedSektorler, setSelectedSektorler] = useState([]);
|
||||
const selectedSektor = selectedSektorler[0] || '';
|
||||
const setSelectedSektor = (v) => setSelectedSektorler(v ? [v] : []);
|
||||
const toggleSektor = (sek) =>
|
||||
setSelectedSektorler((prev) => (prev.includes(sek) ? prev.filter((s) => s !== sek) : [...prev, sek]));
|
||||
|
||||
// Sektör/kurum listeleri sunucudan gelir; sabit liste yalnızca yedektir.
|
||||
const [sektorListesi, setSektorListesi] = useState(SECTORS_LIST);
|
||||
const [kurumTipiListesi, setKurumTipiListesi] = useState(INSTITUTION_TYPES_LIST);
|
||||
useEffect(() => {
|
||||
fetch('/api/meta/filtreler')
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d?.sektorler?.length) setSektorListesi(d.sektorler);
|
||||
if (d?.kurumTipleri?.length) setKurumTipiListesi(d.kurumTipleri);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const [onlyValidOnly, setOnlyValidOnly] = useState(true);
|
||||
const [quickFilter, setQuickFilter] = useState('all');
|
||||
|
||||
@@ -447,7 +467,7 @@ export const AppProvider = ({ children }) => {
|
||||
setSelectedCity('');
|
||||
setSelectedAlimTuru('');
|
||||
setSelectedKurumTipi('');
|
||||
setSelectedSektor('');
|
||||
setSelectedSektorler([]);
|
||||
setQuickFilter('all');
|
||||
};
|
||||
|
||||
@@ -480,6 +500,11 @@ export const AppProvider = ({ children }) => {
|
||||
setSelectedKurumTipi,
|
||||
selectedSektor,
|
||||
setSelectedSektor,
|
||||
selectedSektorler,
|
||||
setSelectedSektorler,
|
||||
toggleSektor,
|
||||
sektorListesi,
|
||||
kurumTipiListesi,
|
||||
onlyValidOnly,
|
||||
setOnlyValidOnly,
|
||||
quickFilter,
|
||||
|
||||
+22
-4
@@ -4,15 +4,33 @@
|
||||
* kategori listeleridir; il-plaka.mjs gibi kanonik sabitlerdir.
|
||||
*/
|
||||
|
||||
// Yedek liste — canlıda /api/meta/filtreler'den gelen liste kullanılır.
|
||||
export const SECTORS_LIST = [
|
||||
"Sağlık / Tıbbi Malzeme",
|
||||
"Kırtasiye & Büro",
|
||||
"Akaryakıt & Yakıt",
|
||||
"Altyapı & Yol",
|
||||
"Asansör & Yürüyen Sistem",
|
||||
"Bilişim & Yazılım",
|
||||
"Danışmanlık & Proje",
|
||||
"Elektrik & Enerji",
|
||||
"Güvenlik Sistemleri",
|
||||
"Gıda & Catering",
|
||||
"Isıtma & İklimlendirme",
|
||||
"İnşaat & Bakım",
|
||||
"Temizlik & Hijyen",
|
||||
"Kırtasiye & Büro",
|
||||
"Laboratuvar & Ölçüm",
|
||||
"Mobilya & Donatı",
|
||||
"Otomotiv & İş Makinesi",
|
||||
"Peyzaj & Park",
|
||||
"Reklam & Organizasyon",
|
||||
"Sağlık / Tıbbi Malzeme",
|
||||
"Sigorta",
|
||||
"Spor Tesisleri",
|
||||
"Su & Arıtma",
|
||||
"Tarım & Hayvancılık",
|
||||
"Taşımacılık & Kiralama",
|
||||
"Tekstil & Giyim",
|
||||
"Otomotiv & Yedek Parça",
|
||||
"Temizlik & Hijyen",
|
||||
"Yayın & Arşiv",
|
||||
];
|
||||
|
||||
export const INSTITUTION_TYPES_LIST = [
|
||||
|
||||
Reference in New Issue
Block a user